# Technical Documentation: ChilliCream/graphql-platform

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/ChilliCream/graphql-platform)

### Overview (README.md)

[](https://www.nuget.org/packages/HotChocolate/)
[](https://github.com/ChilliCream/graphql-platform/blob/main/LICENSE)
[](http://slack.chillicream.com/)
[](https://twitter.com/chilli_cream)


---

# 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](https://slack.chillicream.com), if you would like to get in touch with us, need help, or just want to learn!

## Our Products

### Hot Chocolate

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

[Documentation](https://chillicream.com/docs/hotchocolate/)

### 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](https://get-nitro.chillicream.com), used as a [Web App](https://nitro.chillicream.com), which can be installed through your browser of choice as well, or used as middleware on your GraphQL endpoint. Middlewares are available in [.NET](https://www.nuget.org/packages/HotChocolate.AspNetCore) and [NodeJS](https://www.npmjs.com/package/@chillicream/nitro-express-middleware). More middlewares will follow.

[Documentation](https://chillicream.com/docs/nitro/)

### 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](https://relay.dev/) and [Apollo Client](https://www.apollographql.com/docs/react/), which is nice due to the fact that you can build reactive UIs in .NET with stuff like client-side data caching and data-fetching strategies.

[Documentation](https://chillicream.com/docs/strawberryshake/)

### Green Donut

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

[Documentation](https://chillicream.com/docs/hotchocolate/fetching-data/dataloader/)

## Official examples

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

- [GraphQL workshop](https://github.com/ChilliCream/graphql-workshop)
- [HotChocolate examples](https://github.com/ChilliCream/hotchocolate-examples)

## Contributing

[Become a code contributor](CONTRIBUTING.md) and help us make the ChilliCream GraphQL platform even better!

## From our community

[Check out](COMMUNITY.md) what members of our awesome community have made!

## License

The source code in this repository is licensed under the [MIT License](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](TRADEMARKS.md) for details.

## Financial Contributors

[Become a financial contributor](https://opencollective.com/chillicream/contribute) and help us sustain our community.

### Sponsor

[Become a sponsor](https://opencollective.com/chillicream#contribute) and get your logo on our README on Github with a link to your site.



<a href="https://opencollective.com/chillicream/sponsor/0/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/0/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/1/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/1/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/2/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/2/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/3/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/3/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/4/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/4/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/5/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/5/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/6/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/6/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/7/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/7/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/8/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/8/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/9/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/9/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/10/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/10/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/11/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/11/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/12/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/12/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/13/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/13/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/14/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/14/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/15/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/15/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/16/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/16/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/17/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/17/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/18/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/18/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/sponsor/19/website?requireActive=false" target="_blank" rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/sponsor/19/avatar.svg?requireActive=false"></a>



### Backer

[Become a backer](https://opencollective.com/chillicream#contribute) and get your image on our README on Github with a link to your site.



<a href="https://opencollective.com/chillicream/backer/0/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/0/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/1/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/1/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/2/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/2/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/3/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/3/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/4/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/4/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/5/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/5/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/6/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/6/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/7/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/7/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/8/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/8/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/9/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/9/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/10/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/10/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/11/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/11/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/12/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/12/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/13/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/13/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/14/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/14/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/15/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/15/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/16/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/16/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/17/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/17/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/18/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/18/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/chillicream/backer/19/website?requireActive=false" target="_blank"  rel="noreferrer noopener"><img src="https://opencollective.com/chillicream/backer/19/avatar.svg?requireActive=false"></a>

---

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

---
title: "Getting Started with GraphQL Federation"
description: "Follow a practical GraphQL Federation tutorial to build two subgraphs, compose a unified schema, and query the unified API with ChilliCream's Fusion gateway."
---

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](./index.md).

# What Is Fusion and Why Use It?

Hot Chocolate Fusion lets you split a GraphQL API across multiple independent services, called **subgraphs**, and present them to clients as a single, unified schema (called a **composite schema** in the spec). Each subgraph contributes types and fields to the overall API, runs in its own process, and can be developed, deployed, and scaled independently.

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

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

**When should you use Fusion?** Fusion becomes valuable when your API spans multiple domains (products, reviews, accounts, shipping), when different teams own different parts of the API, or when you need to deploy and scale parts of your API independently. If you find yourself wanting to break a growing monolith into smaller, team-owned services without forcing clients to stitch together calls to multiple endpoints, Fusion is the tool for the job.

# Thinking in Composite Schemas

When you first encounter distributed GraphQL, there is a natural assumption about how subgraphs interact that turns out to be wrong. Getting the right mental model now will save you from the most common design mistakes.

## One GraphQL Schema, Many Contributors

In Fusion, all subgraphs contribute to **one shared GraphQL schema**. There is no "Products Schema" and "Reviews Schema" that exist as separate GraphQL schemas. There is one graph that describes your entire domain, and different subgraphs are responsible for different parts of it.

This means a single type can have fields contributed by multiple subgraphs. For example, the `Product` type might get its `name` and `price` fields from the Products subgraph, its `reviews` field from the Reviews subgraph, and its `deliveryEstimate` field from the Shipping subgraph. From the client's perspective, `Product` is one type with all those fields. The gateway figures out where each field lives and fetches it from the right place.

## Subgraphs Don't Call Each Other

When you split an API across multiple services, it is tempting to think each subgraph needs to call other subgraphs directly. Consider two subgraphs: one that manages Tenants, and one that manages Users. A `User` belongs to a `Tenant`.

**You might assume this is how it works:**

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

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

**How Fusion actually works:**

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

In Fusion, your subgraph never calls another subgraph. Instead, it says: "this field returns a Tenant with this ID" and trusts the gateway to figure out the rest. The gateway knows which subgraph can resolve a Tenant by ID, using a **lookup**, which is a query field that resolves an entity by its key. The Tenants subgraph provides this lookup resolver, and the gateway calls it when it needs to turn a tenant ID into a full Tenant object. The Users subgraph never talks to the Tenants subgraph directly. The gateway handles all coordination.

## Why This Matters

This design has practical consequences:

- **Subgraphs stay independent.** The Users subgraph does not import anything from the Tenants subgraph. It just knows that `Tenant` is an entity with an `id` field.
- **Adding new subgraphs is safe.** If a new Billing subgraph wants to contribute a `billingPlan` field to the `Tenant` type, it can do so without modifying the Tenants or Users subgraphs.
- **The gateway is the coordinator.** Cross-subgraph data fetching is the gateway's job, not yours. Your subgraph only needs to know how to resolve the fields it owns and how to look up its own entities by key. When you build your subgraphs in the following sections, keep this model in mind: each subgraph contributes to one API, declares its entities and lookups, and trusts the gateway to wire everything together.

# Key Concepts

Before diving into code, here are the core terms you will encounter throughout this guide. Each builds on the mental model from the previous section.

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

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

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

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

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

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

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

```graphql
type Query {
  productById(id: ID!): Product @lookup
}
```

- An **internal lookup** (marked with the `@internal` directive) is excluded from the composite schema, cannot be called by clients, and is used only by the gateway for cross-subgraph entity resolution.

```graphql
type Query {
  productById(id: ID!): Product @internal @lookup
}
```

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

```graphql
type User {
  name: String @sharable
}
```

# Prerequisites

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

## .NET SDK

Install the [.NET 10 SDK](https://dotnet.microsoft.com/download) or later. Verify your installation:

```shell
dotnet --version
```

You should see `10.0.100` or higher.

## Nitro CLI

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

```shell
dotnet tool install -g ChilliCream.Nitro.CommandLine
```

Verify the installation:

```shell
nitro version
```

## Hot Chocolate Templates

Install the Hot Chocolate templates:

```shell
dotnet new install HotChocolate.Templates
```

# Create Your First Subgraph (Products)

Time to write code. You will create a Products subgraph that exposes a few products through a GraphQL API. This subgraph will later become part of your composed API.

## Project Setup

Create a new GraphQL server project from the template:

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

dotnet new graphql -n Products
```

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

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

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

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

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

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

</Project>
```

## Configure the Port

The `dotnet new graphql` template already defines a default port in `launchSettings.json`. For this tutorial, change it so the Products subgraph runs on port 5001. Edit `Products/Properties/launchSettings.json` and set `launchUrl` and `applicationUrl` under the `http` profile to:

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

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

## Define the Product Type

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

```csharp
namespace Products;

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

    public required string Name { get; set; }

    public double Price { get; set; }
}
```

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

## Add In-Memory Data

Create a file called `ProductRepository.cs`:

```csharp
namespace Products;

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

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

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

> In production, use **DataLoader** or **batch resolvers** in your subgraphs to batch backend lookups and reduce N+1 query patterns. This is especially important for entity lookups across subgraphs. See the [Fusion demo project](https://github.com/ChilliCream/fusion-demo) for a more complete example.

## Expose Products Through GraphQL

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

```csharp
namespace Products;

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

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

Two attributes to notice:

- **`[QueryType]`** tells Hot Chocolate that this class contributes fields to the `Query` root type. The source generator (from `HotChocolate.Types.Analyzers`) automatically registers these fields.
- **`[Lookup]`** marks `GetProductById` as a lookup resolver. This is how the gateway resolves a `Product` when another subgraph references it. Without this attribute, the gateway would have no way to fetch a Product by its ID from this subgraph. Because this lookup is **public**, it also appears in the composite schema as a query field clients can call directly. Lookups must return nullable entity types (`Product?`) so unresolved keys can return `null` and avoid cascading failures when one or more subgraphs cannot provide requested fields for an entity.

## Configure the Server

Set `Program.cs` to:

```csharp
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

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

This is a minimal Hot Chocolate server. `#!csharp AddGraphQL("Products")` sets the subgraph name used during schema export. `AddTypes()` registers all discovered types, including `ProductQueries` and `Product`. `RunWithGraphQLCommands(args)` enables CLI commands, including schema export.

## Test the Subgraph

Run the Products subgraph:

```shell
cd Products
dotnet run
```

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

```graphql
query {
  products {
    id
    name
    price
  }
}
```

You should see:

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

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

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

Stop the server with `Ctrl+C` before continuing.

## Export the Schema

The gateway needs each subgraph's source schema for composition. Hot Chocolate can export source schema files automatically. From the `fusion-getting-started` directory, run:

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

This generates two files in the Products project directory:

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

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

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

The `name` field identifies this subgraph within the composite schema and must be unique. The `url` is where the gateway sends requests to this subgraph at runtime.

## What You Built

Your Products subgraph now:

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

In the next section, you will create a second subgraph (Reviews) that references Products. This is where the power of Fusion starts to show.

# Create a Second Subgraph (Reviews)

Now you will create a Reviews subgraph that adds review data to the API. The key part is that the Reviews subgraph will add a `reviews` field to the `Product` type that lives in the Products subgraph. This is Fusion's core capability: multiple subgraphs contributing fields to the same type.

## Project Setup

From the `fusion-getting-started` directory:

```shell
dotnet new graphql -n Reviews
```

The template also creates sample files in `Reviews/Types` (`Author.cs`, `Book.cs`, `Query.cs`). Delete those files before continuing.

## Configure the Port

The `dotnet new graphql` template already defines a default port in `launchSettings.json`. For this tutorial, change it so the Reviews subgraph runs on port 5002. Edit `Reviews/Properties/launchSettings.json` and set `launchUrl` and `applicationUrl` under the `http` profile to:

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

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

## Define the Review Type

Create `Review.cs` in the Reviews project:

```csharp
namespace Reviews;

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

    public required string Body { get; set; }

    public int Stars { get; set; }

    public int ProductId { get; set; }
}
```

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

## Add In-Memory Data

Create `ReviewRepository.cs`:

```csharp
namespace Reviews;

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

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

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

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

Notice that reviews reference products by `ProductId`. Reviews 1 and 2 belong to Product 1 (Table), Review 3 belongs to Product 2 (Couch), and Review 4 belongs to Product 3 (Chair).

## Define the Product Stub

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

```csharp
namespace Reviews;

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

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

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

- `id`, `name`, and `price` from the Products subgraph
- `reviews` from the Reviews subgraph

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

## Add Query Resolvers

Create `ReviewQueries.cs`:

```csharp
namespace Reviews;

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

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

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

```csharp
namespace Reviews;

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

Two attributes to notice:

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

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

Notice that this lookup returns `Product?` (nullable), even though it is internal. Lookups should be nullable so unresolved keys can return `null` and avoid cascading failures across subgraphs. In this sample, the resolver still constructs a stub with `new(id)` because the key is already known.

## Configure the Server

Set `Program.cs` to:

```csharp
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

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

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

## Add the ObjectType Extension

There is one more piece needed. The `Review` type currently exposes a raw `ProductId`. To expose `review.product` instead, add a type extension for `Review`. Create `ReviewNode.cs`:

```csharp
using HotChocolate.Types;

namespace Reviews;

[ObjectType<Review>]
public static partial class ReviewNode
{
    [BindMember(nameof(Review.ProductId))]
    public static Product GetProduct([Parent] Review review)
        => new(review.ProductId);
}
```

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

This transformation happens in the GraphQL schema, not your C# classes. Your `Review` class still has a `ProductId` field internally, but in the exported schema that field is replaced with `product: Product`. When a client queries `review.product`, Hot Chocolate calls `GetProduct()`, reads the `ProductId` from the parent Review, and returns a `Product` stub with just that ID. The gateway then uses the Product lookup to fetch the full product data from whichever subgraph owns it.

## Test the Subgraph

Run the Reviews subgraph:

```shell
cd Reviews
dotnet run
```

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

```graphql
query {
  reviews {
    id
    body
    stars
  }
}
```

You should see:

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

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

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

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

Stop the server with `Ctrl+C`.

## Export the Schema

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

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

This generates two files in the Reviews project directory:

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

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

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

The `name` field identifies this subgraph within the composite schema and must be unique. The `url` is where the gateway sends requests to this subgraph at runtime.

## What You Built

Your Reviews subgraph now:

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

You now have two independent subgraphs that contribute to one shared API. In the next section, you will compose them into a single composite schema.

# Compose the Schemas with Nitro CLI

You now have two subgraphs, each with an exported schema and a settings file. The next step is **composition**: merging these source schemas into a single composite schema that the gateway will serve to clients.

Composition is handled by the Nitro CLI. It reads each subgraph's `.graphqls` schema file and its companion `-settings.json` file, validates that the schemas are compatible, and produces a **Fusion archive** (`.far` file) that contains everything the gateway needs: the composite schema, the execution schema with routing metadata, and the transport configuration for each subgraph.

## Verify Your File Structure

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

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

Each subgraph directory has a `schema.graphqls` and a `schema-settings.json`. The Nitro CLI matches these files by their shared prefix (`schema`).

## Run Composition

From the `fusion-getting-started` directory, run:

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

If composition succeeds, you will see output similar to:

```text
Validating source schemas...
Merging schemas...
Fusion archive created: gateway.far
```

The exact output may vary by Nitro CLI version. Confirm that `Gateway/gateway.far` was created. This file contains the composed gateway configuration, and the gateway loads it directly.

## What Happens During Composition

The Nitro CLI performs four steps:

1. **Validate** source schemas: check GraphQL validity and Fusion metadata.
2. **Merge** source schemas: combine matching types and enforce field ownership rules.
3. **Check satisfiability**: verify that cross-subgraph selections can actually be fulfilled with the available keys and lookups.
4. **Produce** output: create the composite schema plus routing metadata for the gateway.

# Run the Fusion Gateway

The gateway is the service that clients connect to. It loads the composed configuration from the `.far` file, exposes the composite schema, and routes queries to the appropriate subgraphs at runtime.

## Create the Gateway Project

From the `fusion-getting-started` directory:

```shell
cd Gateway
dotnet new graphql-gateway
cd ..
```

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

Your `Gateway/Gateway.csproj` should look like this:

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

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

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

</Project>
```

## Configure the Port

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

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

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

## Verify the Fusion Archive

The composition step already wrote `gateway.far` into the `Gateway` directory (`-f Gateway/gateway.far`). Verify that the file exists before continuing:

```shell
ls Gateway/gateway.far
```

## Configure the Gateway

The template generates `Gateway/Program.cs`. For this tutorial, enable operation-plan telemetry so Nitro can show execution metrics in the Operation Plan view:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddHttpClient("fusion");

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

var app = builder.Build();

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

Three things to notice:

- **`#!csharp AddHttpClient("fusion")`** registers a named HTTP client called `"fusion"`. The gateway uses this client to send requests to the subgraphs. The name `"fusion"` is the default HTTP client name that Fusion uses when no explicit `clientName` is specified in the subgraph's `schema-settings.json`.
- **`AddGraphQLGateway()`** registers the Fusion gateway services. This is what makes this project a gateway rather than a regular GraphQL server.
- **`#!csharp AddFileSystemConfiguration("./gateway.far")`** tells the gateway to load its composed configuration from a local file. In production, you would typically use `.AddNitro()` to download the configuration from the Nitro cloud, but for local development the file system approach is simpler.
- **`#!csharp ModifyRequestOptions(o => o.CollectOperationPlanTelemetry = true)`** enables operation-plan telemetry. This is off by default.

## Start Everything

You need all three services running at the same time: both subgraphs and the gateway. Open three terminal windows and start the subgraphs first.

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

**Terminal 1: Products subgraph**

```shell
cd fusion-getting-started/Products
dotnet run
```

**Terminal 2: Reviews subgraph**

```shell
cd fusion-getting-started/Reviews
dotnet run
```

**Terminal 3: Gateway**

```shell
cd fusion-getting-started/Gateway
dotnet run
```

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

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

The port number will differ for each service (5001, 5002, and 5000). If you see errors like "Address already in use," another process is using that port. Either stop it or choose a different port in `launchSettings.json`.

## Verify the Gateway

Open your browser to `http://localhost:5000/graphql/` to access the Nitro GraphQL IDE on the gateway. Try a simple query to verify the gateway is working:

```graphql
query {
  products {
    id
    name
    price
  }
}
```

You should see the same product data as when you queried the Products subgraph directly. The difference is that this query went through the gateway, which routed it to the Products subgraph behind the scenes. If you look at the terminal running the Products subgraph (Terminal 1), you should see a log entry showing it received and processed the request. This confirms the gateway successfully routed the query.

## What You Built

Your gateway now:

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

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

# Query Across Subgraphs

This is the moment everything comes together. With all three services running (Products on 5001, Reviews on 5002, Gateway on 5000), you can now run a single query that fetches data from both subgraphs.

## The Cross-Subgraph Query

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

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

Example in Nitro:



You should see:

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

Look at what happened: `name` and `price` came from the Products subgraph, while `reviews` came from the Reviews subgraph. The client sent one query to one endpoint, and the gateway coordinated the rest.

## What the Gateway Did

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

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



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



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

The client never knew that two separate services were involved. This is the core promise of Fusion: multiple subgraphs, one unified API.

## Try Another Query

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

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

You should see:

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

This time, `body` and `stars` came from the Reviews subgraph, while `product.name` and `product.price` came from the Products subgraph. The gateway resolved the product references by calling the Products subgraph's `productById` lookup with each review's product ID.

## Compare: Before and After Composition

Remember in the previous section, when you queried `review.product` on the Reviews subgraph directly? You could see `product.id` and `product.reviews`, but `product.name` and `product.price` were missing because those fields did not exist in the Reviews subgraph.

Now, through the gateway, `product.name` and `product.price` are available. Composition merged the Product type from both subgraphs, and the gateway resolves each field from the subgraph that owns it. This is what it means for subgraphs to contribute to one shared API.

## Lookup a Single Product

You can also look up a single product by ID:

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

This uses the `productById` lookup from the Products subgraph, the public one that appears in the composite schema. The gateway then fetches reviews from the Reviews subgraph using its internal lookup. Public lookups serve as both client-facing query fields and gateway entity resolution entry points, while internal lookups are only used by the gateway behind the scenes.

## What You Accomplished

You proved that Fusion works:

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

The Products and Reviews subgraphs are completely independent. They do not import each other's code, do not call each other directly, and can be deployed and scaled separately. Yet clients can query across both as if they were one unified schema.

# Sharing Fields Across Subgraphs

In the tutorial so far, the Products and Reviews subgraphs each contribute **different** fields to the `Product` type. Products owns `name` and `price`, while Reviews owns `reviews`. There is no overlap, so composition works without any special annotations.

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

The `[Shareable]` attribute solves this. When you mark a field as shareable in both subgraphs, you are telling Fusion: "these definitions are intentional and return the same data. Use whichever is most efficient."

## When You Need It

A common scenario: your Reviews subgraph needs to display the product name alongside each review. You could always fetch the name from the Products subgraph via the gateway, but if performance matters, you might want the Reviews subgraph to cache product names locally. In that case, both subgraphs would define `Product.name`:

**Products subgraph:**

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

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

    public double Price { get; set; }
}
```

**Reviews subgraph (entity stub):**

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

    public List<Review> GetReviews()
        => ReviewRepository.GetByProductId(Id);
}
```

Both subgraphs define `Product.name` and mark it `[Shareable]`. Composition succeeds, and the gateway can resolve `name` from either subgraph depending on what else the query needs. If a query only asks for `product.name` and `product.reviews`, the gateway will fetch everything from the Reviews subgraph in a single call instead of making a separate trip to the Products subgraph.

## The Rule

Without `[Shareable]`, a non-key field must exist in exactly one subgraph. Key fields (like `id`) are automatically shareable, so you do not need the attribute on them. For any other field that appears in multiple subgraphs, add `[Shareable]` to **all** definitions of that field. If even one subgraph forgets to mark it, composition fails.

## When Not to Use It

Do not mark a field as shareable unless the subgraphs genuinely return the same data. If two subgraphs define `Product.name` but one returns the display name and the other returns an internal code name, marking them shareable would give clients inconsistent results depending on which subgraph the gateway happens to call.

If the fields return **different** data, they should have **different** names (e.g., `displayName` vs `codeName`) and each lives in its own subgraph without `[Shareable]`.

# What's Next

You now have a working Fusion setup: two subgraphs contributing to one composed API, served through a single gateway. Here are some directions to explore next based on what you need:

- **I want to add another subgraph to this project**: [Adding a Subgraph](./adding-a-subgraph.md)
- **I want to understand entities more deeply**: [Entities and Lookups](./entities-and-lookups.md)
- **I need cross-subgraph field dependencies (`[Require]`)**: [Entities and Lookups](./entities-and-lookups.md)
- **I need to deploy this**: [Deployment & CI/CD](./deployment-and-ci-cd.md)
- **I need to secure this**: [Authentication and Authorization](./authentication-and-authorization.md)
- **I'm coming from Apollo**: [Coming from Apollo Federation](./migration/coming-from-apollo-federation.md)

---

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

---
title: Fusion
metaTitle: "Fusion: Federated GraphQL Gateway"
description: "Fusion is ChilliCream's GraphQL gateway for composing multiple services into one API, implementing the GraphQL Composite Schemas spec with build-time validation."
---

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](https://graphql.github.io/composite-schemas-spec/draft/), 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](https://graphql.github.io/composite-schemas-spec/draft/) is designed so a standard GraphQL server can already act as a compatible subgraph. In a common Hot Chocolate setup, subgraphs remain normal Hot Chocolate servers with regular resolvers, without a separate distributed-runtime package or vendor-specific protocol layer.

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

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


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


# Key Terminology

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

# When to Use Fusion

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

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

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

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

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

# When NOT to Use Fusion

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

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

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

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

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

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

# Migrating from a Monolith

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

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

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

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

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

# Next Steps

Where you go from here depends on what you need:

- **"I want to build something."** Start with the [Getting Started](./getting-started.md) 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](./adding-a-subgraph.md). 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](./migration/coming-from-apollo-federation.md) or [Migrating from Schema Stitching](./migration/migrating-from-schema-stitching.md). These guides map familiar concepts to Fusion equivalents and walk through a migration.

- **"I need to deploy this."** See [Deployment & CI/CD](./deployment-and-ci-cd.md) for pipeline setup, schema management, and gateway configuration.

- **"I need CDN and browser caching behavior."** See [Cache Control](./cache-control.md) 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"
description: "Hot Chocolate is an open-source GraphQL server for .NET that turns your C# classes into a spec-compliant schema and handles parsing, validation, and execution."
---

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

# What Is Hot Chocolate

Hot Chocolate is a GraphQL server framework that runs on [ASP.NET Core](https://learn.microsoft.com/aspnet/core). You write C# types and resolvers. Hot Chocolate turns them into a GraphQL schema, validates incoming operations against that schema, executes them, and returns results over HTTP, WebSocket, or Server-Sent Events.

Hot Chocolate implements the [GraphQL 2025 specification](https://spec.graphql.org/) and several draft features including `@defer`, `@stream`, and `@requiresOptIn`. It implements the [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/) for transport. It is compatible with all spec-compliant clients, including [Strawberry Shake](../strawberryshake/index.md), [Relay](https://relay.dev/), and [Apollo Client](https://www.apollographql.com/docs/react/).

# How You Build a Schema

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

## Implementation-First

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

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

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

## Code-First

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

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

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

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

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

# GraphQL API Security Strategies

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

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

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

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

## First-party GraphQL

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

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

- [Trusted documents](./performance/trusted-documents.md) covers the full workflow: extraction, registration, and enforcement.
- [Strawberry Shake](../strawberryshake/index.md) and [Relay](https://relay.dev/docs/guides/persisted-queries/) both support build-time operation extraction.

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

## Third-party GraphQL

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

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

- [Cost analysis](./security/cost-analysis.md) explains field weights, type costs, and budget configuration.
- [Controlling introspection](./security/introspection.md) 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](https://ibm.github.io/graphql-specs/cost-spec.html). |
| **Trusted documents** | Pre-registered operations that the server accepts by hash. Operations not in the store are rejected. Also known as persisted operations.                                                                                  |

# Scaling Beyond a Single Server

When your API grows beyond what a single service can handle, [Fusion](../fusion/index.md) 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](./get-started-with-graphql-in-net-core.md) tutorial. You will create a running GraphQL server in under five minutes.

- **"I want to understand the schema system."** Read [Defining a Schema](./defining-a-schema/index.md). It covers queries, mutations, subscriptions, and all the GraphQL types.

- **"I need to fetch data efficiently."** Go to [DataLoader](./fetching-data/batching/dataloader.md) for batching and caching, or [Resolvers](./resolvers/index.md) for the full resolver API.

- **"I need to secure my API."** See [Securing Your API](./security/index.md) for authentication, authorization, cost analysis, and trusted documents.

- **"I'm migrating from an older version."** Read the [migration guide from v15 to v16](./migrating/migrate-from-15-to-16.md).

- **"I want to split my API across services."** See [Fusion](../fusion/index.md) 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"
description: "Mocha is a messaging framework for .NET with a message bus for inter-service communication and a source-generated mediator for in-process CQRS."
---

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

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

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

# What Mocha is

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

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

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

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

# Terminology

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

| Term                 | Definition                                                                                                                                                                                                                              |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Message**          | A plain C# record representing business data. The unit of communication between handlers and services.                                                                                                                                  |
| **Event**            | A message published via `PublishAsync`. Represents something that happened. Multiple handlers can receive an event.                                                                                                                     |
| **Request**          | A message sent via `SendAsync` or `RequestAsync`. When sent with `RequestAsync`, the sender awaits a typed response. When sent with `SendAsync`, it is fire-and-forget.                                                                 |
| **Handler**          | A class implementing a Mocha handler interface that processes a specific message type.                                                                                                                                                  |
| **Consumer**         | The processing unit that wraps a handler or custom logic. Mocha builds consumers automatically from handlers, or you can implement `IConsumer<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](./mediator/index.md) and the [message bus](./handler-registration.md).                           |
| **Command**          | A mediator message representing an action. Implements `ICommand` (void) or `ICommand<TResponse>` (with response). Dispatched via `SendAsync`.                                                                                           |
| **Query**            | A mediator message representing a read operation. Implements `IQuery<TResponse>`. Dispatched via `QueryAsync`.                                                                                                                          |

# Architecture

When you call `PublishAsync`, here is what happens:

<MochaTopologyVisualization data='{"services":[{"host":{"serviceName":"OrderService","assemblyName":"OrderService.dll","instanceId":"order-svc-1"},"messageTypes":[{"identity":"msg:OrderPlaced","runtimeType":"OrderPlaced","runtimeTypeFullName":"MyApp.Messages.OrderPlaced","isInterface":false,"isInternal":false}],"consumers":[],"routes":{"inbound":[],"outbound":[{"kind":"publish","messageTypeIdentity":"msg:OrderPlaced","endpoint":{"name":"e/order-placed","address":"rabbitmq://localhost/e/order-placed","transportName":"RabbitMQ"}}]},"sagas":[]},{"host":{"serviceName":"BillingService","assemblyName":"BillingService.dll","instanceId":"billing-svc-1"},"messageTypes":[{"identity":"msg:OrderPlaced","runtimeType":"OrderPlaced","runtimeTypeFullName":"MyApp.Messages.OrderPlaced","isInterface":false,"isInternal":false}],"consumers":[{"name":"OrderPlacedHandler","identityType":"OrderPlacedHandler","identityTypeFullName":"MyApp.Handlers.OrderPlacedHandler"}],"routes":{"inbound":[{"kind":"subscribe","messageTypeIdentity":"msg:OrderPlaced","consumerName":"OrderPlacedHandler","endpoint":{"name":"billing.order-placed","address":"rabbitmq://localhost/billing.order-placed","transportName":"RabbitMQ"}}],"outbound":[]},"sagas":[]}],"transports":[{"identifier":"rabbitmq://localhost:5672/","name":"RabbitMQ","schema":"rabbitmq","transportType":"RabbitMQMessagingTransport","receiveEndpoints":[{"name":"billing.order-placed","kind":"default","address":"rabbitmq://localhost/billing.order-placed","source":{"address":"rabbitmq://localhost:5672/q/billing.order-placed"}}],"dispatchEndpoints":[{"name":"e/order-placed","kind":"default","address":"rabbitmq://localhost/e/order-placed","destination":{"address":"rabbitmq://localhost:5672/e/order-placed"}}],"topology":{"address":"rabbitmq://localhost:5672/","entities":[{"kind":"exchange","name":"order-placed","address":"rabbitmq://localhost:5672/e/order-placed","flow":"inbound","properties":{"type":"fanout","durable":true,"autoDelete":false,"autoProvision":true}},{"kind":"exchange","name":"billing.order-placed","address":"rabbitmq://localhost:5672/e/billing.order-placed","flow":"inbound","properties":{"type":"fanout","durable":true,"autoDelete":false,"autoProvision":true}},{"kind":"queue","name":"billing.order-placed","address":"rabbitmq://localhost:5672/q/billing.order-placed","flow":"outbound","properties":{"durable":true,"exclusive":false,"autoDelete":false,"autoProvision":true}}],"links":[{"kind":"bind","address":"rabbitmq://localhost:5672/b/e/order-placed/e/billing.order-placed","source":"rabbitmq://localhost:5672/e/order-placed","target":"rabbitmq://localhost:5672/e/billing.order-placed","direction":"forward","properties":{"routingKey":null,"autoProvision":true}},{"kind":"bind","address":"rabbitmq://localhost:5672/b/e/billing.order-placed/q/billing.order-placed","source":"rabbitmq://localhost:5672/e/billing.order-placed","target":"rabbitmq://localhost:5672/q/billing.order-placed","direction":"forward","properties":{"routingKey":null,"autoProvision":true}}]}}]}' trace='{"traceId":"intro-event-flow-trace-001","activities":[{"id":"ie-1","parentId":null,"startTime":"2024-06-15T10:30:00.000Z","durationMs":45,"status":"ok","operation":"publish","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","transport":"RabbitMQ"},{"id":"ie-2","parentId":"ie-1","startTime":"2024-06-15T10:30:00.001Z","durationMs":2,"status":"ok","operation":"dispatch","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","endpointName":"e/order-placed","endpointAddress":"rabbitmq://localhost/e/order-placed","transport":"RabbitMQ"},{"id":"ie-3","parentId":"ie-2","startTime":"2024-06-15T10:30:00.012Z","durationMs":3,"status":"ok","operation":"receive","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","endpointName":"billing.order-placed","endpointAddress":"rabbitmq://localhost/billing.order-placed","transport":"RabbitMQ"},{"id":"ie-4","parentId":"ie-3","startTime":"2024-06-15T10:30:00.015Z","durationMs":12,"status":"ok","operation":"consume","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","consumerName":"OrderPlacedHandler"}]}' />

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

# Core capabilities

## Handler-first design

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

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

Mocha provides handler interfaces for each messaging pattern:

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

## Messaging patterns and consumers

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

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

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

## Pluggable transports

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

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

## Middleware pipelines

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

## OpenTelemetry-native observability

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

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

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

<MochaTopologyVisualization data='{"services":[{"host":{"serviceName":"OrderApi","assemblyName":"OrderApi.dll","instanceId":"order-api-1"},"messageTypes":[{"identity":"msg:OrderPlaced","runtimeType":"OrderPlaced","runtimeTypeFullName":"MyApp.Messages.OrderPlaced","isInterface":false,"isInternal":false}],"consumers":[],"routes":{"inbound":[],"outbound":[{"kind":"publish","messageTypeIdentity":"msg:OrderPlaced","endpoint":{"name":"e/order-placed","address":"rabbitmq://localhost/e/order-placed","transportName":"RabbitMQ"}}]},"sagas":[]},{"host":{"serviceName":"BillingService","assemblyName":"BillingService.dll","instanceId":"billing-svc-1"},"messageTypes":[{"identity":"msg:OrderPlaced","runtimeType":"OrderPlaced","runtimeTypeFullName":"MyApp.Messages.OrderPlaced","isInterface":false,"isInternal":false}],"consumers":[{"name":"CreateInvoiceHandler","identityType":"CreateInvoiceHandler","identityTypeFullName":"MyApp.Billing.CreateInvoiceHandler"}],"routes":{"inbound":[{"kind":"subscribe","messageTypeIdentity":"msg:OrderPlaced","consumerName":"CreateInvoiceHandler","endpoint":{"name":"billing.create-invoice","address":"rabbitmq://localhost/billing.create-invoice","transportName":"RabbitMQ"}}],"outbound":[]},"sagas":[]},{"host":{"serviceName":"InventoryService","assemblyName":"InventoryService.dll","instanceId":"inventory-svc-1"},"messageTypes":[{"identity":"msg:OrderPlaced","runtimeType":"OrderPlaced","runtimeTypeFullName":"MyApp.Messages.OrderPlaced","isInterface":false,"isInternal":false}],"consumers":[{"name":"ReserveStockHandler","identityType":"ReserveStockHandler","identityTypeFullName":"MyApp.Inventory.ReserveStockHandler"}],"routes":{"inbound":[{"kind":"subscribe","messageTypeIdentity":"msg:OrderPlaced","consumerName":"ReserveStockHandler","endpoint":{"name":"inventory.reserve-stock","address":"rabbitmq://localhost/inventory.reserve-stock","transportName":"RabbitMQ"}}],"outbound":[]},"sagas":[]}],"transports":[{"identifier":"rabbitmq://localhost:5672/","name":"RabbitMQ","schema":"rabbitmq","transportType":"RabbitMQMessagingTransport","receiveEndpoints":[{"name":"billing.create-invoice","kind":"default","address":"rabbitmq://localhost/billing.create-invoice","source":{"address":"rabbitmq://localhost:5672/q/billing.create-invoice"}},{"name":"inventory.reserve-stock","kind":"default","address":"rabbitmq://localhost/inventory.reserve-stock","source":{"address":"rabbitmq://localhost:5672/q/inventory.reserve-stock"}}],"dispatchEndpoints":[{"name":"e/order-placed","kind":"default","address":"rabbitmq://localhost/e/order-placed","destination":{"address":"rabbitmq://localhost:5672/e/order-placed"}}],"topology":{"address":"rabbitmq://localhost:5672/","entities":[{"kind":"exchange","name":"order-placed","address":"rabbitmq://localhost:5672/e/order-placed","flow":"inbound","properties":{"type":"fanout","durable":true,"autoDelete":false,"autoProvision":true}},{"kind":"exchange","name":"billing.create-invoice","address":"rabbitmq://localhost:5672/e/billing.create-invoice","flow":"inbound","properties":{"type":"fanout","durable":true,"autoDelete":false,"autoProvision":true}},{"kind":"exchange","name":"inventory.reserve-stock","address":"rabbitmq://localhost:5672/e/inventory.reserve-stock","flow":"inbound","properties":{"type":"fanout","durable":true,"autoDelete":false,"autoProvision":true}},{"kind":"queue","name":"billing.create-invoice","address":"rabbitmq://localhost:5672/q/billing.create-invoice","flow":"outbound","properties":{"durable":true,"exclusive":false,"autoDelete":false,"autoProvision":true}},{"kind":"queue","name":"inventory.reserve-stock","address":"rabbitmq://localhost:5672/q/inventory.reserve-stock","flow":"outbound","properties":{"durable":true,"exclusive":false,"autoDelete":false,"autoProvision":true}}],"links":[{"kind":"bind","address":"rabbitmq://localhost:5672/b/e/order-placed/e/billing.create-invoice","source":"rabbitmq://localhost:5672/e/order-placed","target":"rabbitmq://localhost:5672/e/billing.create-invoice","direction":"forward","properties":{"routingKey":null,"autoProvision":true}},{"kind":"bind","address":"rabbitmq://localhost:5672/b/e/order-placed/e/inventory.reserve-stock","source":"rabbitmq://localhost:5672/e/order-placed","target":"rabbitmq://localhost:5672/e/inventory.reserve-stock","direction":"forward","properties":{"routingKey":null,"autoProvision":true}},{"kind":"bind","address":"rabbitmq://localhost:5672/b/e/billing.create-invoice/q/billing.create-invoice","source":"rabbitmq://localhost:5672/e/billing.create-invoice","target":"rabbitmq://localhost:5672/q/billing.create-invoice","direction":"forward","properties":{"routingKey":null,"autoProvision":true}},{"kind":"bind","address":"rabbitmq://localhost:5672/b/e/inventory.reserve-stock/q/inventory.reserve-stock","source":"rabbitmq://localhost:5672/e/inventory.reserve-stock","target":"rabbitmq://localhost:5672/q/inventory.reserve-stock","direction":"forward","properties":{"routingKey":null,"autoProvision":true}}]}}]}' trace='{"traceId":"otel-real-world-trace-001","activities":[{"id":"ow-1","parentId":null,"startTime":"2024-06-15T10:30:00.000Z","durationMs":85,"status":"ok","operation":"publish","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","transport":"RabbitMQ"},{"id":"ow-2","parentId":"ow-1","startTime":"2024-06-15T10:30:00.001Z","durationMs":3,"status":"ok","operation":"dispatch","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","endpointName":"e/order-placed","endpointAddress":"rabbitmq://localhost/e/order-placed","transport":"RabbitMQ"},{"id":"ow-3","parentId":"ow-2","startTime":"2024-06-15T10:30:00.014Z","durationMs":4,"status":"ok","operation":"receive","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","endpointName":"billing.create-invoice","endpointAddress":"rabbitmq://localhost/billing.create-invoice","transport":"RabbitMQ"},{"id":"ow-4","parentId":"ow-3","startTime":"2024-06-15T10:30:00.018Z","durationMs":32,"status":"ok","operation":"consume","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","consumerName":"CreateInvoiceHandler"},{"id":"ow-5","parentId":"ow-2","startTime":"2024-06-15T10:30:00.016Z","durationMs":3,"status":"ok","operation":"receive","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","endpointName":"inventory.reserve-stock","endpointAddress":"rabbitmq://localhost/inventory.reserve-stock","transport":"RabbitMQ"},{"id":"ow-6","parentId":"ow-5","startTime":"2024-06-15T10:30:00.019Z","durationMs":45,"status":"ok","operation":"consume","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","consumerName":"ReserveStockHandler"}]}' />

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

## Resiliency

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

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

## Saga orchestration

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

<MochaTopologyVisualization data='{"services":[{"host":{"serviceName":"OrderService","assemblyName":"OrderService.dll","instanceId":"order-svc-1"},"messageTypes":[{"identity":"msg:RequestQuickRefundRequest","runtimeType":"RequestQuickRefundRequest","runtimeTypeFullName":"MyApp.Messages.RequestQuickRefundRequest","isInterface":false,"isInternal":false},{"identity":"msg:ProcessRefundCommand","runtimeType":"ProcessRefundCommand","runtimeTypeFullName":"MyApp.Messages.ProcessRefundCommand","isInterface":false,"isInternal":false},{"identity":"msg:ProcessRefundResponse","runtimeType":"ProcessRefundResponse","runtimeTypeFullName":"MyApp.Messages.ProcessRefundResponse","isInterface":false,"isInternal":false},{"identity":"msg:QuickRefundResponse","runtimeType":"QuickRefundResponse","runtimeTypeFullName":"MyApp.Messages.QuickRefundResponse","isInterface":false,"isInternal":false}],"consumers":[],"routes":{"inbound":[{"kind":"request","messageTypeIdentity":"msg:RequestQuickRefundRequest","consumerName":"QuickRefundSagaConsumer","endpoint":{"name":"saga-endpoint","address":"loopback://localhost/q/saga-endpoint","transportName":"InMemory"}},{"kind":"reply","messageTypeIdentity":"msg:ProcessRefundResponse","consumerName":"QuickRefundSagaConsumer","endpoint":{"name":"saga-reply","address":"loopback://localhost/q/saga-reply","transportName":"InMemory"}}],"outbound":[{"kind":"send","messageTypeIdentity":"msg:ProcessRefundCommand","endpoint":{"name":"billing-handler","address":"loopback://localhost/q/billing-handler","transportName":"InMemory"}}]},"sagas":[{"name":"QuickRefundSaga","stateType":"RefundSagaState","stateTypeFullName":"MyApp.Sagas.RefundSagaState","consumerName":"QuickRefundSagaConsumer","states":[{"name":"Initial","isInitial":true,"isFinal":false,"onEntry":{},"transitions":[{"eventType":"RequestQuickRefundRequest","eventTypeFullName":"MyApp.Messages.RequestQuickRefundRequest","transitionTo":"AwaitingRefund","transitionKind":"request","autoProvision":false,"send":[{"messageType":"ProcessRefundCommand","messageTypeFullName":"MyApp.Messages.ProcessRefundCommand"}]}]},{"name":"AwaitingRefund","isInitial":false,"isFinal":false,"onEntry":{},"transitions":[{"eventType":"ProcessRefundResponse","eventTypeFullName":"MyApp.Messages.ProcessRefundResponse","transitionTo":"Completed","transitionKind":"reply","autoProvision":false}]},{"name":"Completed","isInitial":false,"isFinal":true,"onEntry":{},"response":{"eventType":"QuickRefundResponse","eventTypeFullName":"MyApp.Messages.QuickRefundResponse"},"transitions":[]}]}]},{"host":{"serviceName":"BillingService","assemblyName":"BillingService.dll","instanceId":"billing-svc-1"},"messageTypes":[{"identity":"msg:ProcessRefundCommand","runtimeType":"ProcessRefundCommand","runtimeTypeFullName":"MyApp.Messages.ProcessRefundCommand","isInterface":false,"isInternal":false},{"identity":"msg:ProcessRefundResponse","runtimeType":"ProcessRefundResponse","runtimeTypeFullName":"MyApp.Messages.ProcessRefundResponse","isInterface":false,"isInternal":false}],"consumers":[{"name":"ProcessRefundCommandHandler","identityType":"ProcessRefundCommandHandler","identityTypeFullName":"MyApp.Handlers.ProcessRefundCommandHandler"}],"routes":{"inbound":[{"kind":"request","messageTypeIdentity":"msg:ProcessRefundCommand","consumerName":"ProcessRefundCommandHandler","endpoint":{"name":"billing-handler","address":"loopback://localhost/q/billing-handler","transportName":"InMemory"}}],"outbound":[{"kind":"send","messageTypeIdentity":"msg:ProcessRefundResponse","endpoint":{"name":"saga-reply","address":"loopback://localhost/q/saga-reply","transportName":"InMemory"}}]},"sagas":[]}],"transports":[{"identifier":"inmemory","name":"InMemory","schema":"loopback","transportType":"InMemoryTransport","receiveEndpoints":[{"name":"saga-endpoint","kind":"default","address":"loopback://localhost/q/saga-endpoint","source":{"address":"loopback://localhost/q/saga-endpoint"}},{"name":"billing-handler","kind":"default","address":"loopback://localhost/q/billing-handler","source":{"address":"loopback://localhost/q/billing-handler"}},{"name":"saga-reply","kind":"reply","address":"loopback://localhost/q/saga-reply","source":{"address":"loopback://localhost/q/saga-reply"}}],"dispatchEndpoints":[{"name":"saga-endpoint","kind":"default","address":"loopback://localhost/q/saga-endpoint","destination":{"address":"loopback://localhost/q/saga-endpoint"}},{"name":"billing-handler","kind":"default","address":"loopback://localhost/q/billing-handler","destination":{"address":"loopback://localhost/q/billing-handler"}},{"name":"saga-reply","kind":"reply","address":"loopback://localhost/q/saga-reply","destination":{"address":"loopback://localhost/q/saga-reply"}}],"topology":{"address":"loopback://localhost","entities":[{"kind":"queue","name":"saga-endpoint","address":"loopback://localhost/q/saga-endpoint","flow":"inbound","properties":{"type":"request"}},{"kind":"queue","name":"billing-handler","address":"loopback://localhost/q/billing-handler","flow":"inbound","properties":{"type":"request"}},{"kind":"queue","name":"saga-reply","address":"loopback://localhost/q/saga-reply","flow":"outbound","properties":{"type":"reply"}}],"links":[]}}]}' trace='{"traceId":"saga-orchestration-trace-001","activities":[{"id":"so-1","parentId":null,"startTime":"2024-06-15T10:30:00.000Z","durationMs":120,"status":"ok","operation":"send","messageType":"RequestQuickRefundRequest","messageTypeIdentity":"msg:RequestQuickRefundRequest","transport":"InMemory"},{"id":"so-2","parentId":"so-1","startTime":"2024-06-15T10:30:00.001Z","durationMs":2,"status":"ok","operation":"dispatch","messageType":"RequestQuickRefundRequest","messageTypeIdentity":"msg:RequestQuickRefundRequest","endpointName":"saga-endpoint","endpointAddress":"loopback://localhost/q/saga-endpoint","transport":"InMemory"},{"id":"so-3","parentId":"so-2","startTime":"2024-06-15T10:30:00.008Z","durationMs":3,"status":"ok","operation":"receive","messageType":"RequestQuickRefundRequest","messageTypeIdentity":"msg:RequestQuickRefundRequest","endpointName":"saga-endpoint","endpointAddress":"loopback://localhost/q/saga-endpoint","transport":"InMemory"},{"id":"so-4","parentId":"so-3","startTime":"2024-06-15T10:30:00.011Z","durationMs":25,"status":"ok","operation":"consume","messageType":"RequestQuickRefundRequest","messageTypeIdentity":"msg:RequestQuickRefundRequest","consumerName":"QuickRefundSagaConsumer"},{"id":"so-5","parentId":"so-4","startTime":"2024-06-15T10:30:00.012Z","durationMs":22,"status":"ok","operation":"saga-transition","sagaName":"QuickRefundSaga","sagaInstanceId":"3f2504e0-4f89-11d3-9a0c-0305e82c3301","fromState":"Initial","toState":"AwaitingRefund","eventType":"RequestQuickRefundRequest"},{"id":"so-6","parentId":"so-5","startTime":"2024-06-15T10:30:00.013Z","durationMs":2,"status":"ok","operation":"dispatch","messageType":"ProcessRefundCommand","messageTypeIdentity":"msg:ProcessRefundCommand","endpointName":"billing-handler","endpointAddress":"loopback://localhost/q/billing-handler","transport":"InMemory"},{"id":"so-7","parentId":"so-6","startTime":"2024-06-15T10:30:00.020Z","durationMs":3,"status":"ok","operation":"receive","messageType":"ProcessRefundCommand","messageTypeIdentity":"msg:ProcessRefundCommand","endpointName":"billing-handler","endpointAddress":"loopback://localhost/q/billing-handler","transport":"InMemory"},{"id":"so-8","parentId":"so-7","startTime":"2024-06-15T10:30:00.023Z","durationMs":20,"status":"ok","operation":"consume","messageType":"ProcessRefundCommand","messageTypeIdentity":"msg:ProcessRefundCommand","consumerName":"ProcessRefundCommandHandler"},{"id":"so-9","parentId":"so-8","startTime":"2024-06-15T10:30:00.043Z","durationMs":2,"status":"ok","operation":"dispatch","messageType":"ProcessRefundResponse","messageTypeIdentity":"msg:ProcessRefundResponse","endpointName":"saga-reply","endpointAddress":"loopback://localhost/q/saga-reply","transport":"InMemory"},{"id":"so-10","parentId":"so-9","startTime":"2024-06-15T10:30:00.050Z","durationMs":3,"status":"ok","operation":"receive","messageType":"ProcessRefundResponse","messageTypeIdentity":"msg:ProcessRefundResponse","endpointName":"saga-reply","endpointAddress":"loopback://localhost/q/saga-reply","transport":"InMemory"},{"id":"so-11","parentId":"so-10","startTime":"2024-06-15T10:30:00.053Z","durationMs":15,"status":"ok","operation":"consume","messageType":"ProcessRefundResponse","messageTypeIdentity":"msg:ProcessRefundResponse","consumerName":"QuickRefundSagaConsumer"},{"id":"so-12","parentId":"so-11","startTime":"2024-06-15T10:30:00.054Z","durationMs":12,"status":"ok","operation":"saga-transition","sagaName":"QuickRefundSaga","sagaInstanceId":"3f2504e0-4f89-11d3-9a0c-0305e82c3301","fromState":"AwaitingRefund","toState":"Completed","eventType":"ProcessRefundResponse"}]}' />

Mocha persists saga state, manages transitions, and supports compensation when steps fail. See [Sagas](./sagas.md) for a full walkthrough.

## In-process mediator

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

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

public class PlaceOrderCommandHandler(AppDbContext db)
    : ICommandHandler<PlaceOrderCommand, PlaceOrderResult>
{
    public async ValueTask<PlaceOrderResult> HandleAsync(
        PlaceOrderCommand command, CancellationToken cancellationToken)
    {
        // business logic
        return new PlaceOrderResult(true, Guid.NewGuid());
    }
}
```

```csharp
// Register and use
builder.Services
    .AddMediator()
    .AddCatalog()
    .UseEntityFrameworkTransactions<AppDbContext>();

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

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

# Learning paths

Choose an entry point based on how you learn best:

- **Get something running first:** [Quick Start](./quick-start.md) -zero to a working message bus in under five minutes with the InMemory transport.
- **Understand the concepts first:** [Messages](./messages.md) then [Messaging Patterns](./messaging-patterns.md) - learn what flows through the system and what patterns govern how it flows.
- **Evaluating Mocha for a specific broker:** [Transports](./transports/index.md) - understand the transport abstraction and what is available.
- **In-process CQRS:** [Mediator](./mediator/index.md) - dispatch commands, queries, and notifications within a single service using the source-generated mediator.

- **See a real-world system:** The [Demo application](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo) is a complete e-commerce system with three services (Catalog, Billing, Shipping) that demonstrates event-driven communication, sagas, batch processing, the transactional outbox, and .NET Aspire orchestration.

Ready to build? Start with the [Quick Start](./quick-start.md).

---

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

---
title: "Getting Started"
description: "Set up Nitro as a web or desktop app and run your first GraphQL query, from creating a document to connecting to a server and executing operations."
---

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

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

# Web App

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

# Desktop App

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

Upon successful installation, follow the steps in the [Your first Query](#your-first-query) guide to execute your first GraphQL query using Nitro.

# Your First Query

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

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


**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](https://workshop.chillicream.com/graphql)
It should resemble this:


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

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


Take note of the following key elements:

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

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

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

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

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


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

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



**Step 10:** Great job! You've successfully created, executed, and saved your first GraphQL query using Nitro. To learn more about the Nitro User Interface, head over to the [Explore the UI](./explore-the-ui/index.md) guide.

Happy querying!

---

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

---
title: Nitro
metaTitle: "Nitro: GraphQL IDE, Observability, and Governance"
description: "Introduction to Nitro, the GraphQL IDE from ChilliCream with team collaboration, schema and client registries, and OpenTelemetry monitoring for your APIs."
---

Nitro combines a collaborative [GraphQL IDE](./explore-the-ui/index.md), [schema and client registries](./apis/overview.md), and [OpenTelemetry monitoring](./open-telemetry/operation-monitoring.md) 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!](./getting-started.md)

# Features

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

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

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

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

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

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

---

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

---
title: "Getting Started"
description: "Run the `skills` CLI with `dnx` and add your first Agent Skill to Claude Code or another AI coding agent in five minutes using `dnx skills add`."
---

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

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

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

# Prerequisites

Before you start, make sure you have the following:

| Requirement                                                   | Why you need it                                                                                                                                                             |
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [.NET 10 SDK or newer](https://dotnet.microsoft.com/download) | To run `skills` with `dnx`, the way these docs show it. The `dnx` command ships with the .NET 10 SDK.                                                                       |
| An AI coding agent                                            | The target for your skill. This guide uses [Claude Code](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview). The `skills` CLI supports 55+ agents. |

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

# Run `skills`

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

## Run with dnx

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

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

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

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

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

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

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

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

| Command                                      | What it does                        |
| -------------------------------------------- | ----------------------------------- |
| `#!shell dnx skills@0.3.0 add ...`           | Runs an exact version.              |
| `#!shell dnx skills@0.3.* 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](https://learn.microsoft.com/dotnet/core/tools/global-tools). This needs the .NET SDK 8.0 or newer.

```shell
dotnet tool install -g skills
```

```text
You can invoke the tool using the following command: skills
Tool 'skills' (version '0.3.0') was successfully installed.
```

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

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

```shell
dotnet new tool-manifest
dotnet tool install skills
```

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

# Add your first skill

You are ready to install a skill. The example below installs skills from [anthropics/skills](https://github.com/anthropics/skills), Anthropic's reference repository, targeting Claude Code.

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

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

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

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

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

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

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

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

**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](./troubleshooting.md) for more.

# See what is installed

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

```shell
dnx skills list
```

```text
Project Skills

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

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

# Use the skill

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

Your agent loads each installed skill's `name` and `description` at startup, then reads the full skill only when a task matches the description. This is [progressive disclosure](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview): the description is the trigger, and the body loads on demand. Start a task that matches the skill, and your agent picks it up automatically.

# Update or remove the global tool

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

To update the global `skills` tool to the latest version:

```shell
dotnet tool update -g skills
```

```text
Tool 'skills' was successfully updated from version '0.3.0' to version '<new>'.
```

To uninstall it:

```shell
dotnet tool uninstall -g skills
```

```text
Tool 'skills' (version '0.3.0') was successfully uninstalled.
```

Note that `dotnet tool update` updates the `skills` CLI, not your installed skills. To check your skills for updates, use the `dnx skills update` command, covered in [Installing Skills](./installing-skills.md).

# Next steps

- [Installing Skills](./installing-skills.md): every source form, project versus global scope, picking agents and skills, symlink versus copy, and checking for skill updates.
- [Authoring Skills](./authoring-skills.md): scaffold a `SKILL.md` with `dnx skills init`, write a good description, and publish your skill.
- [Reference](./reference.md): 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"
description: "The `skills` .NET CLI installs, updates, and authors Agent Skills: portable SKILL.md files you can share across Claude Code, Cursor, and 55+ agents."
---

`skills` is the .NET CLI that installs, updates, and authors [Agent Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills): portable `SKILL.md` files that teach an AI coding agent how you work. Stop re-explaining your conventions every session. Package the knowledge once, then hand the same skill to Claude Code, Cursor, GitHub Copilot, or another of the 55+ supported agents with a single command.

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

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

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

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

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

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

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

Need the prerequisites or a step-by-step walkthrough? See [Get started](./getting-started.md). The [source repository](https://github.com/ChilliCream/skills) and [`skills` package on NuGet](https://www.nuget.org/packages/skills) are public.

# What are Agent Skills

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

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

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

The `SKILL.md` format is an open standard created by Anthropic and adopted across the ecosystem. The same file works in 55+ supported agents, including Claude Code, Cursor, and GitHub Copilot. To go deeper, read Anthropic's [Agent Skills overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) and the [open specification](https://agentskills.io/specification) at agentskills.io.

# How skills load: progressive disclosure

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

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

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

The open standard frames the same flow as Discovery, Activation, and Execution. For the full model, see Anthropic's [overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) and the [agentskills.io home page](https://agentskills.io/home).

# What a skill looks like

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

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

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

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

# Roll dice

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

The `dnx skills init` command scaffolds this layout for you; [Authoring Skills](./authoring-skills.md) walks through it with the generated `SKILL.md` and folder tree. The full field reference, including optional frontmatter, lives in the [open specification](https://agentskills.io/specification) and in [Authoring Skills](./authoring-skills.md).

# Why Skills

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

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

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

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

# The ecosystem

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

| Layer         | What it is                                | Where                                                                                                             |
| ------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Format / spec | The open `SKILL.md` standard              | [agentskills.io](https://agentskills.io/specification), [anthropics/skills](https://github.com/anthropics/skills) |
| Discovery     | A directory of published skills to browse | [skills.sh](https://www.skills.sh/)                                                                               |
| Installers    | CLIs that fetch and install skills        | `skills` (.NET), [npx skills](https://github.com/vercel-labs/skills) (Node)                                       |

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

# Next steps

- [Get started](./getting-started.md): run `skills` and add your first skill end to end.
- [Author a skill](./authoring-skills.md): 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"
description: A reactive 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](../hotchocolate/index.md), [Apollo Server](https://www.apollographql.com/docs/apollo-server), and [GraphQL Java](https://www.graphql-java.com).

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](./get-started/index.md) 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"
description: "Overview of the Hot Chocolate type system: how C# classes map to GraphQL SDL for objects, inputs, enums, interfaces, unions, and scalars in your schema."
---

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

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

# Understanding the Schema as the API Contract

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# Schema Authoring Styles

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

## Implementation-First

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

Common building blocks include:

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

## Code-First

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

```csharp
using HotChocolate.Types;

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

# 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](./queries.md)             |
| Mutation     | Write entry point. Top-level mutation fields execute serially.                         | `[MutationType]` or `AddMutationType`.         | [Mutations](./mutations.md)         |
| Subscription | Event stream entry point.                                                              | `[SubscriptionType]` or `AddSubscriptionType`. | [Subscriptions](./subscriptions.md) |

## 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](./object-types.md) |
| Scalar      | Leaf values such as `String`, `Int`, `Boolean`, `ID`, `UUID`, `URI`, `DateTime`, `Any`, and custom scalars. | [Scalars](./scalars.md)           |
| Enum        | A closed set of symbolic values in input or output positions.                                               | [Enums](./enums.md)               |
| Interface   | Polymorphic output types that share fields.                                                                 | [Interfaces](./interfaces.md)     |
| Union       | Polymorphic output types that do not need shared fields.                                                    | [Unions](./unions.md)             |

## 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](./queries.md), [Mutations](./mutations.md), [Object Types](./object-types.md) |
| Argument | A value supplied to a field. Common uses include lookup IDs, filters, paging arguments, and mutation payloads.      | [Arguments](./arguments.md)                                                             |

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](./arguments.md)                   |
| Input object type | Structured payloads for mutations, filters, and other complex field inputs. | [Input Object Types](./input-object-types.md) |

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](./lists.md) |
| List                    | `[Book]`   | The value is a collection.                              | [Lists and Non-Null](./lists.md) |
| List plus item non-null | `[Book!]!` | The list is required, and every item is required.       | [Lists and Non-Null](./lists.md) |

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](./object-types.md)       |
| Relay helpers   | Use stable IDs, global object identification, `node`, `nodes`, `[ID]`, `[Node]`, and `[NodeResolver]`.   | [Relay](./relay.md)                     |
| Dynamic schemas | Generate types from CMS, multi-tenant, or configuration-driven metadata with `ITypeModule`.              | [Dynamic Schemas](./dynamic-schemas.md) |

## 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](./documentation.md) |
| Directives                | Add schema metadata or executable behavior, depending on the directive kind.        | [Directives](./directives.md)                |
| Deprecation and evolution | Communicate lifecycle changes and plan compatible schema updates.                   | [Versioning](./versioning.md)                |

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](./queries.md).
- Learn how returned shapes are inferred and configured in [Object Types](./object-types.md).
- Add field inputs with [Arguments](./arguments.md) and [Input Object Types](./input-object-types.md).
- Strengthen the contract with [Lists and Non-Null](./lists.md).
- Move to runtime behavior with [Resolvers](../resolvers/index.md) and [DataLoader](../fetching-data/batching/dataloader.md) after the schema shape is clear.

---

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

---
title: Fetching Data
description: "Overview of data middleware in Hot Chocolate: pagination, filtering, sorting, projections, and DataLoader batching applied to IQueryable data sources."
---

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

# Pagination

Hot Chocolate provides cursor-based connection pagination out of the box. Connections follow the [Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm), giving clients a standardized way to page through large datasets. When backed by `IQueryable`, pagination translates directly to native database queries.

[Learn more about pagination](./pagination.md)

# Filtering

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

[Learn more about filtering](./filtering.md)

# Sorting

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

[Learn more about sorting](./sorting.md)

# Projections

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

[Learn more about projections](./projections.md)

# 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](./batching/dataloader.md) for key-based batching with deduplication and caching.
- [Batch Resolvers](./batching/batch-resolver.md) 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](./integrations/entity-framework.md) for EF Core DbContext patterns and pooling.
- [MongoDB](./integrations/mongodb.md) for the MongoDB driver integration.
- [Marten](./integrations/marten.md) for Marten document database support.
- [Extending Filtering](./integrations/extending-filtering.md) for building custom filter providers.

---

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

---
title: Performance
description: "Overview of Hot Chocolate performance features: faster startup, trusted documents, and persisted operations that cut parsing and validation overhead per request."
---

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](../server/warmup.md)

# Trusted Documents

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

Hot Chocolate supports two flavors of persisted operations.

## Regular Persisted Operations

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

Strawberry Shake, [Relay](https://relay.dev/docs/guides/persisted-queries/), and [Apollo](https://www.apollographql.com/docs/react/api/link/persisted-queries/) client all support this approach.

[Learn more about persisted operations](./trusted-documents.md)

## Automatic Persisted Operations

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

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

Both Strawberry Shake and [Apollo](https://www.apollographql.com/docs/apollo-server/performance/apq/) client support this approach.

[Learn more about automatic persisted operations](./automatic-persisted-operations.md)

---

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

---
title: Resolvers
metaTitle: "GraphQL Resolvers in Hot Chocolate"
description: "Introduction to resolvers in Hot Chocolate: how the resolver tree executes a query, defining resolver methods, accessing arguments, and injecting services."
---

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

# The Resolver Tree

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

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

This produces the following resolver tree:

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

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

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

# Defining a Resolver

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

<ExampleTabs>
<Implementation>

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

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

This generates the following schema:

```sdl
type Query {
  foo: String!
}
```

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

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

public record User(string Name);
```

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

</Implementation>
<Code>

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

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

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

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

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

</Code>
</ExampleTabs>

## 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.

<ExampleTabs>
<Implementation>

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

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

</Implementation>
<Code>

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

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

        // Omitted code for brevity
    });
```

</Code>
</ExampleTabs>

# 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.

<ExampleTabs>
<Implementation>

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

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

</Implementation>
<Code>

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

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

        // Omitted code for brevity
    });
```

</Code>
</ExampleTabs>

[Learn more about arguments](../defining-a-schema/arguments.md)

# Injecting Services

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

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

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

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

[Learn more about dependency injection](./dependency-injection.md)

# Accessing parent values

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

For example, consider the following schema:

```sdl
type Query {
  me: User!;
}

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

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

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

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

Access the parent value like this:

<ExampleTabs>
<Implementation>

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

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

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

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

Use `nameof` to make this requirement refactoring-safe.

</Implementation>
<Code>

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

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

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

                // Omitted code for brevity
            });
    }
}
```

</Code>
</ExampleTabs>

# What's in This Section

- [Dependency Injection](./dependency-injection.md) explains how services are injected into resolvers, scoping behavior, keyed services, and switching the service provider.
- [Errors](./errors.md) covers how exceptions become GraphQL errors, error filters for mapping domain exceptions, and throwing `GraphQLException` for explicit errors.
- [Field Middleware](./field-middleware.md) 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
description: "Overview of securing a Hot Chocolate GraphQL API: cost analysis for public APIs, trusted documents for private ones, plus authorization and request limits."
---

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](./cost-analysis.md)

# 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](../performance/trusted-documents.md)

# 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](./authentication.md)

## 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](./authorization.md)

## 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](./request-limits.md)

## 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](./introspection.md#disabling-introspection)

## FIPS Compliance

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

```csharp
builder
    .AddGraphQL()
    .AddSha256DocumentHashProvider();
```

[Learn more about hashing providers](../performance/trusted-documents.md#hashing-algorithms)

# Next Steps

- **Building a public API?** Start with [Cost Analysis](./cost-analysis.md).
- **Building a private API?** Start with [Trusted Documents](../performance/trusted-documents.md).
- **Need authentication?** See [Authentication](./authentication.md).
- **Need authorization?** See [Authorization](./authorization.md).

---

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

---
title: Server
description: "Overview of configuring and operating a Hot Chocolate GraphQL server: endpoints, HTTP transport, interceptors, dependency injection, and instrumentation."
---

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](/products/nitro) GraphQL IDE.

[Learn more about endpoints](./endpoints.md)

# 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](./http-transport.md)

# 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](./cache-control.md)

# Interceptors

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

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

[Learn more about interceptors](./interceptors.md)

# 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](../resolvers/dependency-injection.md)

# Warmup

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

[Learn more about warmup](./warmup.md)

# Global State

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

[Learn more about global state](./global-state.md)

# 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](../security/introspection.md)

# 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](./files.md)

# 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](./instrumentation.md)

# Batching

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

[Learn more about batching](./batching.md)

# 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](./command-line.md)

---

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

---
title: Mediator
description: "Use the Mocha Mediator to dispatch commands, queries, and notifications within a single process using zero-reflection, source-generated dispatch."
---

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

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

# What the mediator is

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

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

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

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

If you have used [MediatR](https://github.com/jbogard/MediatR), the concepts are familiar. Mocha Mediator takes a different approach to performance: a [Roslyn source generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) analyzes your handler registrations at compile time and produces pre-compiled pipeline delegates. No `MakeGenericType`, no service provider lookups to resolve the pipeline, no reflection at runtime.

# When to use the mediator vs. the message bus

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

| Use the **mediator** when...                                                                                           | Use the **message bus** when...                     |
| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| Dispatch stays in-process                                                                                              | Messages cross process or service boundaries        |
| You want [CQRS](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs) separation of commands and queries | You want pub/sub events across services             |
| You need a request/response pipeline with middleware                                                                   | You need transport-level features (retries, outbox) |
| Handlers live in the same assembly or solution                                                                         | Handlers live in different services                 |

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

# Messages

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

## Commands

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

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

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

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

## Queries

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

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

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

## Notifications

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

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

## Message type reference

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

# Handlers

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

## Command handlers

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

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

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

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

## Query handlers

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

## Notification handlers

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

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

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

## Handler interface reference

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

# Dispatching messages

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

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

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

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

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

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

## Untyped dispatch

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

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

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

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

# Registration and source generation

## Register the mediator

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

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

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

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

## Module naming

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

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

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

```csharp
using Mocha.Mediator;

[assembly: MediatorModule("Billing")]
```

## Manual handler registration with AddHandler

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

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

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

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

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

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

## Configure service lifetime

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

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

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

## Configuration options reference

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

# Named mediators

To run multiple independent mediator instances (each with its own handlers and middleware), use named mediators. Named mediators use .NET's [keyed dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#keyed-services).

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

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

Resolve a named mediator from DI:

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

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

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

# Putting it together

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

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

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

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

# Troubleshooting

## `InvalidOperationException: No pipeline registered for message type`

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

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

## Handlers are not being called

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

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

## The source-generated method does not appear

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

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

## `InvalidOperationException` when calling `AddHandler<T>()`

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](./pipeline-and-middleware.md) - 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"
description: "Understand how transports move messages in Mocha, how the transport abstraction works, and how to choose between InMemory, PostgreSQL, RabbitMQ, and Azure Service Bus."
---

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

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

Mocha ships with four transports:

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

# Add a transport

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

```csharp
// InMemory - zero configuration
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddInMemory();
```

```csharp
// RabbitMQ - production-ready
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddRabbitMQ();
```

```csharp
// PostgreSQL - database-backed transport
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddPostgres("Host=localhost;Database=mocha_messaging;Username=postgres;Password=postgres");
```

```csharp
// Azure Service Bus - managed cloud messaging
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddAzureServiceBus(connectionString);
```

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

# Choose a transport

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

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

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

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

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

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

# Scope and middleware

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

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

To add middleware at the transport level:

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

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

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

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

# Customize queues and binding

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

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

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

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

# Control implicit and explicit binding

By default, transports bind handlers implicitly using naming conventions:

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

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

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

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

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

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

# Claim handlers for a transport

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

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

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

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

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

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

# Use multiple transports

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

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

You can also use the queue descriptor with explicit binding:

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

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

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

# Next steps

- [InMemory Transport](./in-memory.md) - Set up the InMemory transport for development and testing.
- [PostgreSQL Transport](./postgres.md) - Configure database-backed messaging with PostgreSQL.
- [RabbitMQ Transport](./rabbitmq.md) - Configure the RabbitMQ transport for production deployments.
- [Azure Service Bus Transport](./azure-service-bus.md) - Configure managed cloud messaging on Azure.

> **Runnable example:** [MultiTransport](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Transports/MultiTransport)

---

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

---
title: APIs
description: "APIs in Nitro represent your GraphQL servers and group documents, settings, registries, and telemetry. Compare Collection, Service, and Gateway API types."
---


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](./schema-registry.md) guide
the [Client Registry](./client-registry.md) and the [Telemetry](../open-telemetry/operation-monitoring.md) guide.

# API Types



## API Collection



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

## API Service


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

## API Gateway


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

# Creating an API



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

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

---

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

---
title: Documents
description: "Tour of the Document View in Nitro, the workspace for running GraphQL queries, mutations, and subscriptions and exploring the schema reference and SDL."
---

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](./schema-reference.md) 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](./schema-definition.md) 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](./operations.md) 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](./response.md) 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](./connection-settings.md) guide for detailed information about it. You can also configure the authentication settings for the document. Refer to the [Authentication Settings](./authentication.md) 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
description: "Overview of the Nitro user interface: sidebar, document tree, main area, environments, history, settings, and the status bar, with links to detailed guides."
---



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

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

2. **Environments**: This section is for managing environments.

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

4. **Settings**: To modify application preferences, you can navigate to the Settings section. Learn more about it in our [Settings Guide](../settings/index.md).

5. **User Menu**: This section allows users to sign in/out, manage their accounts, and handle billing. You can read more about account management and billing in the organization section [Organizations](../organizations/index.md).

6. **Document Tree**: Here you can manage files, folders, documents, and APIs. Read more about it in the [Explorer](./explorer.md).

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

8. **Status Bar**: The status bar provides quick access to workspace, organization, and synchronization features. It also displays additional information. More details are provided in our [Status Bar Guide](./status-bar.md).

---

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

---
title: "Organizations"
description: "Create and manage organizations in Nitro, the structure that hosts your workspaces and members, including the personal organization and management portal."
---

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](https://identity.chillicream.com/Organizations) to open the management portal and press 'Manage' on the organization that you like to manage.

# Switching Organizations

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



# Managing Members



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

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

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

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

## Joining an Organization

Invited users can join an organization in two ways:

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

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

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

# User Roles and Access

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

| Role         | Transfer Ownership | Delete Organization | Invite Users | View Subscriptions | Add Redirect URLs |
| ------------ | ------------------ | ------------------- | ------------ | ------------------ | ----------------- |
| Owner        | Yes                | Yes                 | Yes          | Yes                | Yes               |
| Admin        | No                 | No                  | Yes          | Yes                | Yes               |
| Collaborator | No                 | No                  | No           | No                 | No                |

- **Owner:** The owner can transfer ownership, delete the organization, and has all the permissions of an admin. Each organization can only have one owner.

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

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

## Managing Permissions



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

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

2. Select the desired role for the member.

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

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

# Transferring Ownership

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


# Redirect URLs

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

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

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

You can add the redirect URL in two ways:

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



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



In the portal you have more options to configure the allowed destinations. You can for example allow all localhost origins. Or disable the access to the web UI in the [cloud](https://nitro.chillicream.com)

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

---

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

---
title: Settings - Overview
description: "Overview of the Settings section in Nitro, reached via the cog icon, covering customization options like Themes and where to find organization settings."
---



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](./themes.md) 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](../organizations/index.md) documentation.

---

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

---
title: "Caching"
description: "Strawberry Shake caches GraphQL results in a normalized entity store, reducing network requests and keeping reactive components in sync with state."
---

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

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

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

# Strategies

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

## Network Only

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

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

## Cache First

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

## Cache and Network

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

## Configuration

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

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

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

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

---

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

---
title: "Get started with Strawberry Shake and Blazor"
description: "Add a Strawberry Shake GraphQL client to a Blazor WebAssembly app: generate typed C# clients from GraphQL queries and build reactive components."
---

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

[Watch the video on YouTube](https://www.youtube.com/watch?v=-oq7YEciouM)

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

In this tutorial, we will teach you:

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

# Step 1: Add the Strawberry Shake CLI tools

The Strawberry Shake tool will help you to set up your project to create a GraphQL client.

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

1. Create a dotnet tool-manifest.

```shell
dotnet new tool-manifest
```

2. Install the Strawberry Shake tools.

```shell
dotnet tool install StrawberryShake.Tools --local
```

# Step 2: Create a Blazor WebAssembly project

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

1. First, a new solution called `Demo.sln`.

```shell
dotnet new sln --name Demo
```

2. Create a new Blazor for WebAssembly application.

```shell
dotnet new blazorwasm --name Demo
```

3. Add the project to the solution `Demo.sln`.

```shell
dotnet sln add ./Demo
```

# Step 3: Install the required package

Strawberry Shake supports multiple GraphQL transport protocols. In this example we will use the standard GraphQL over HTTP protocol to interact with our GraphQL server. All of this functionality comes packaged with the `StrawberryShake.Blazor` meta package.

1. Add the `StrawberryShake.Blazor` package to your project.

```shell
dotnet add Demo package StrawberryShake.Blazor
```

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

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

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

> If you want to have a look at our demo GraphQL server head over [here](https://demo.chillicream.com/graphql).

1. Add the crypto client to your Blazor application.

```shell
dotnet graphql init https://demo.chillicream.com/graphql/ --clientName CryptoClient --Path ./Demo
```

2. Customize the namespace of the generated client to be `Demo.GraphQL`. For this head over to the `.graphqlrc.json` and insert a namespace property in the `StrawberryShake` section.

```json
{
  "schema": "schema.graphql",
  "documents": "**/*.graphql",
  "extensions": {
    "strawberryShake": {
      "name": "CryptoClient",
      "namespace": "Demo.GraphQL",
      "url": "https://demo.chillicream.com/graphql/",
      "records": {
        "inputs": false,
        "entities": false
      },
      "transportProfiles": [
        {
          "default": "Http",
          "subscription": "WebSocket"
        }
      ]
    }
  }
}
```

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

3. Choose your favorite IDE and the solution. If your are using VSCode do the following:

```shell
code ./Demo
```

4. Create new query document `GetSessions.graphql` with the following content:

```graphql
query GetAssets {
  assets {
    nodes {
      name
      price {
        lastPrice
      }
    }
  }
}
```

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

5. Compile your project.

```shell
dotnet build
```

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



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

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

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

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

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

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

await builder.Build().RunAsync();
```

7. Go to `_Imports.razor` and add `Demo.GraphQL` to the common imports

```csharp
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using Demo
@using Demo.Shared // (from .NET 8, `Demo.Layout`)
@using Demo.GraphQL
@using Demo.GraphQL.Components
@using StrawberryShake
```

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

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

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

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

```csharp
@page "/"
```

3. Add the `UseGetAssets` component to your page.

```csharp
@page "/"

<UseGetAssets Context="result">
    <ChildContent>

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

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

4. With that done let's render the actual content.

```csharp
@page "/"

<UseGetAssets Context="result">
    <ChildContent>
        <ul>
        @foreach (var item in result.Assets!.Nodes!)
        {
            <li>@item.Name (@item.Price.LastPrice)</li>
        }
        </ul>
    </ChildContent>
    <ErrorContent>
        Something went wrong ...<br />
        @result.First().Message
    </ErrorContent>
    <LoadingContent>
        Loading ...
    </LoadingContent>
</UseGetAssets>
```

5. Start the Blazor application with `#!shell dotnet watch --project ./Demo` and see if your code works.



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

---

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

---
title: "Networking"
description: "Strawberry Shake networking: HTTP, WebSocket, SignalR, gRPC, and in-memory transport packages, plus transport profiles to pick a protocol per operation."
---

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](https://github.com/michaelstaib/graphql-over-http)                                            | [StrawberryShake.Transport.Http](https://www.nuget.org/packages/StrawberryShake.Transport.Http)             | 11.1                     |
| WebSocket | [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) | [StrawberryShake.Transport.WebSockets](https://www.nuget.org/packages/StrawberryShake.Transport.WebSockets) | 11.1                     |
| WebSocket | [graphql-transport-ws](https://github.com/graphql/graphql-over-http/pull/140)                                     | 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](https://www.nuget.org/packages/StrawberryShake.Transport.InMemory)     | 11.1                     |

# Transport Profiles

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

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

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

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

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

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

---

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

---
title: "Performance"
description: "Optimize Strawberry Shake client performance with persisted operations and persisted state, cutting request size and network round trips for your app."
---

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](./persisted-operations.md).

## 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](../caching/index.md).

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](./persisted-state.md).

---

### 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"
description: "Learn how batching solves the N+1 problem in GraphQL. Hot Chocolate offers DataLoader and batch resolvers to group data fetches into single calls."
---

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](./dataloader.md) and [batch resolvers](./batch-resolver.md).

> [!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](../../server/batching.md).

# 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](../../resolvers/index.md), and the execution engine walks the query as a tree. Consider this query:

**Client query**

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

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

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

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

# DataLoader

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

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

[Learn more about DataLoader](./dataloader.md)

# 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](./batch-resolver.md)

# Next Steps

- **Loading data by key?** Start with [DataLoader](./dataloader.md).
- **Resolving one field for many parents?** See [Batch Resolvers](./batch-resolver.md).
- **New to resolvers?** See [Resolvers](../../resolvers/index.md) for the resolver tree mental model.
- **Using Entity Framework?** See [Entity Framework](../integrations/entity-framework.md) for integration patterns.
- **Looking for HTTP request batching?** See [Server Batching](../../server/batching.md).

---

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

---
title: Integrations
description: Learn how to use the IExecutable interface to abstract data sources in Hot Chocolate.
---

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

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

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

public class Query
{
    public IExecutable<User> GetUsers(IUserRepository repo) =>
        repo.FindAll();
}
```

```sdl
type Query {
    users: [User!]!
}
```

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

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

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

# API

## Source

```csharp
object Source { get; }
```

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

## ToListAsync

```csharp
ValueTask<IList> ToListAsync(CancellationToken cancellationToken);
```

Returns a list of items.

## FirstOrDefaultAsync

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

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

## SingleOrDefaultAsync

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

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

## Print

```csharp
string Print();
```

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

# Example

The following shows the Entity Framework implementation:

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

    object IExecutable.Source => Source;

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

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

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

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

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

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

# Next Steps

- [Entity Framework integration](./entity-framework.md) for EF Core executables
- [MongoDB integration](./mongodb.md) for MongoDB executables
- [Filtering](../filtering.md) 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.

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

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

## Find the process

```bash
dotnet-trace ps
```

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

## CPU sampling trace

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

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

## Allocation trace

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

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

## Heap snapshots

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

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

## Live counters

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

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

## dotTrace

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

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

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

The same session from a script, waiting for the confirmations instead of typing:

```bash
LOG="$OUT/dottrace.log"; FIFO=$(mktemp -u); mkfifo "$FIFO"
dottrace attach <pid> --profiling-type=Sampling --save-to="$OUT/dottrace-sampling.dtp" --overwrite \
  --collect-data-from-start=off --service-input=stdin --service-output=on < "$FIFO" > "$LOG" 2>&1 &
exec 3>"$FIFO"
until grep -q '"connected"' "$LOG"; do sleep 1; done
printf '\n##dotTrace["start"]\r\n' >&3
sleep 30
printf '\n##dotTrace["get-snapshot"]\r\n' >&3
until grep -q '"snapshot-saved"' "$LOG"; do sleep 1; done
printf '\n##dotTrace["disconnect"]\r\n' >&3
wait; exec 3>&-; rm -f "$FIFO"
```

Tracing, for call counts, starts the application under the profiler instead of attaching. Point it at the application binary, not at `dotnet run`, so the profiled process is the application itself:

```bash
dottrace start --framework=NetCore --profiling-type=Tracing --save-to="$OUT/dottrace-tracing.dtp" \
  --collect-data-from-start=off --service-input=stdin --service-output=on \
  bin/Release/net10.0/<App>
```

A snapshot is the `.dtp` file plus its numbered `.dtp.0000` companions. Keep them together.

## dotMemory

One snapshot now:

```bash
dotmemory get-snapshot <pid> --save-to-dir="$OUT/dotmemory"
```

Attach and take a snapshot every 30 seconds, at most three, then detach:

```bash
dotmemory attach <pid> --trigger-timer=30s --trigger-max-snapshots=3 --timeout=2m --save-to-dir="$OUT/dotmemory"
```

`--trigger-mem-inc=50mb` and `--trigger-mem-limit=1gb` trigger on memory growth instead of time. The result is a `.dmw` workspace that holds all snapshots of the session.

## Dumps and symbols

`dotnet-dump collect -p <pid> -o "$OUT/full.dmp"` writes a full process dump for `dotnet-dump analyze`. `dotnet-symbol` downloads the runtime symbols a dump from another machine needs; dumps taken inside the container already resolve.

## Caveats

- Profile Release builds. Debug builds disable optimizations and distort both time and allocations.
- Sampling gives time but not call counts. Use dotTrace Tracing mode for call counts, which means starting the process with `dottrace start`.
- On Linux, EventPipe samples measure thread time, so idle thread pool frames such as `LowLevelLifoSemaphore.WaitForSignal`, `WaitHandle.WaitOneNoCheck`, `Monitor.Wait`, the finalizer thread in `GC.RunFinalizers`, `PollGCWorker` and a blocked file watcher in `Interop+Sys.Read` dominate the exclusive time. Ignore them and read the frames below them. The speedscope export puts a synthetic `CPU_TIME` or `UNMANAGED_CODE_TIME` leaf under every sample, and `?!?` stands for native frames without symbols, mostly GC threads. For the CPU a process really uses, read `dotnet.process.cpu.time` from `dotnet-counters`.
- Take the dotMemory snapshots last. In two sessions the profiled Fusion gateway crashed 45 seconds and two minutes after `dotmemory attach` had disconnected, under load, with a native fault that the runtime turned into a `createdump` minidump. Set `DOTNET_DbgEnableMiniDump=1` on the application if you want that dump, and restart the application after a dotMemory session before measuring anything else.
- `.dtp` and `.dmw` snapshots are for Rider on the host. Agents should use the `dotnet-trace` and `dotnet-gcdump` outputs, which are readable in the container.
- Attaching needs no special container privileges. It only requires that the application was not started with `DOTNET_EnableDiagnostics=0`.

---

### Updating Dotnet (`docs/updating-dotnet.md`)

# Updating .NET

What to check when `TargetFrameworks` in `src/Directory.Build.props` or the SDK version in `global.json` changes. Each section covers code that follows the source of ASP.NET Core or .NET, so an upstream change can call for the same change here.

## Multipart form reader

`MultipartFormReader` in `src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers` reads the form of a multipart request for `HttpMultipartMiddleware`. It follows the multipart branch of `FormFeature.InnerReadFormAsync`, and its antiforgery check, in `src/Http/Http/src/Features/FormFeature.cs` on `dotnet/aspnetcore`. `MultipartReader`, `FileBufferingReadStream`, `FormFile`, and header parsing stay the framework's, so their fixes reach applications through servicing releases.

The reader was last compared with these tags, whose multipart branches are identical:

| Target framework | Tag                      |
| ---------------- | ------------------------ |
| `net8.0`         | `v8.0.31`                |
| `net9.0`         | `v9.0.20`                |
| `net10.0`        | `v10.0.12`               |
| `net11.0`        | `v11.0.0-rc.1.26425.128` |

To list the upstream changes between a recorded tag and a new one, here from `v10.0.12` to `v10.0.13`:

```bash
git clone --filter=blob:none --no-checkout https://github.com/dotnet/aspnetcore.git
git -C aspnetcore log --oneline v10.0.12..v10.0.13 -- src/Http/Http/src/Features/FormFeature.cs
```

Apply a change to the section loop, its limits, the file buffering, or the antiforgery check to the reader, and record the new tag in the table.

The reader differs from `FormFeature` in these ways:

- Each limit has its own error code. `MultipartBodyLengthLimit` applies to every section (`HC0135`), and `maxAllowedRequestSize` also applies to the `operations` field (`HC0010`); whichever of the two is smaller limits `operations` and gives its code. `BufferBodyLengthLimit`, when `BufferBody` is set, applies to the whole body (`HC0136`). `FormFeature` does not apply `maxAllowedRequestSize`; it throws `InvalidDataException` for `MultipartBodyLengthLimit` and `IOException` for `BufferBodyLengthLimit`.
- With `BufferBody` set, it buffers the body even when an earlier component already made the body seekable, so `BufferBodyLengthLimit` applies in that case too.
- It refuses a request whose `IAntiforgeryValidationFeature` has `IsValid` set to `false`, without the `HttpContext.Items` keys that `FormFeature` checks first.
- It does not read URL-encoded forms, which never reach the multipart middleware.
- It does not apply endpoint `IFormOptionsMetadata`, which the public `FormFeature` constructor does not apply either.

---

### Website Content Docs Fusion Adding A Subgraph (`website/content/docs/fusion/adding-a-subgraph.md`)

---
title: "Adding a Subgraph"
description: "Add a new subgraph to an existing Hot Chocolate Fusion project: create the GraphQL service, define entity extensions, export the schema, compose, and verify."
---

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](./getting-started.md) tutorial first.

# Prerequisites

Before you begin, you need:

- An existing Fusion project with at least one subgraph and a gateway
- The [Nitro CLI](./cli.md) installed (`#!shell dotnet tool install -g ChilliCream.Nitro.CommandLine`)
- The .NET 10 SDK or later

You should be able to compose and run your existing project successfully. If composition is currently broken, fix that first.

# Subgraph Project Structure

Every Fusion subgraph follows the same layout:

```text
src/
  Shipping/
    Types/
      Product.cs              # Entity stub
      ShipmentNode.cs         # Type extension (replaces foreign key with entity reference)
      ShippingQueries.cs      # Query resolvers (lookups)
    Shipment.cs               # Domain type
    ShipmentRepository.cs     # In-memory data
    Program.cs                # Server configuration
    Shipping.csproj           # Project file
    appsettings.json          # App settings
    schema-settings.json      # Fusion subgraph settings (created on first schema export)
    schema.graphqls           # Exported schema (generated, do not edit)
```

Your folder structure may differ, but the Fusion components are always the same: a GraphQL server, your type definitions, an exported schema, and a `schema-settings.json`.

# Create the Subgraph Project

Create a new GraphQL server project from the template:

```shell
dotnet new graphql -n Shipping
```

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

Your `Shipping/Shipping.csproj` should look like this:

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

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

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

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

</Project>
```

## Configure the Port

The `dotnet new graphql` template already defines a default port in `launchSettings.json`. Change it so the Shipping subgraph runs on port 5003. Edit `Shipping/Properties/launchSettings.json` and set `launchUrl` and `applicationUrl` under the `http` profile to:

```json
"launchUrl": "http://localhost:5003/graphql",
"applicationUrl": "http://localhost:5003"
```

This ensures the subgraph runs on port 5003, which matches what you will configure in `schema-settings.json` later.

# Define Your Types

The Shipping subgraph owns shipment data and contributes a `shipments` field to the existing `Product` type. It does not own Product. The Products subgraph does. The Shipping subgraph extends it with shipping information.

## Define the Shipment Type

Create `Shipment.cs` in the Shipping project:

```csharp filename="Shipping/Shipment.cs"
namespace Shipping;

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

    public int ProductId { get; set; }

    public required string TrackingNumber { get; set; }

    public required string Status { get; set; }
}
```

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

## Add In-Memory Data

Create `ShipmentRepository.cs`:

```csharp filename="Shipping/ShipmentRepository.cs"
namespace Shipping;

public static class ShipmentRepository
{
    private static readonly List<Shipment> Shipments =
    [
        new Shipment { Id = 1, ProductId = 1, TrackingNumber = "SH-001", Status = "Delivered" },
        new Shipment { Id = 2, ProductId = 1, TrackingNumber = "SH-002", Status = "In Transit" },
        new Shipment { Id = 3, ProductId = 2, TrackingNumber = "SH-003", Status = "Shipped" },
    ];

    public static Shipment? GetById(int id)
        => Shipments.FirstOrDefault(s => s.Id == id);

    public static List<Shipment> GetByProductId(int productId)
        => Shipments.Where(s => s.ProductId == productId).ToList();
}
```

## Create the Entity Stub

An entity stub is a lightweight declaration that says "I know this type exists in the graph and I want to add fields to it." Create `Types/Product.cs`:

```csharp filename="Shipping/Types/Product.cs"
namespace Shipping;

public sealed record Product(int Id)
{
    public List<Shipment> GetShipments()
        => ShipmentRepository.GetByProductId(Id);
}
```

This is not a duplicate of the Product type from the Products subgraph. It is an entity stub. The Shipping subgraph does not define `name`, `price`, or any other Product field. It only contributes the `shipments` field. When the gateway composes the schema, it merges this stub with the full `Product` type from the Products subgraph. Clients see one `Product` type with all fields from all subgraphs.

## Add Query Resolvers

Create `Types/ShippingQueries.cs` with a public lookup for `Shipment` and an internal lookup for `Product`:

```csharp filename="Shipping/Types/ShippingQueries.cs"
namespace Shipping;

[QueryType]
public static partial class ShippingQueries
{
    [Lookup]
    public static Shipment? GetShipmentById(int id)
        => ShipmentRepository.GetById(id);

    [Internal, Lookup]
    public static Product? GetProductById(int id)
        => new(id);
}
```

- `GetShipmentById` is a **public lookup**. Clients can call it directly, and the gateway uses it for entity resolution.
- `GetProductById` is an **internal lookup**. It is hidden from the composite schema and exists only for the gateway to enter the Shipping subgraph's `Product` type during entity resolution. It constructs a stub from the ID without checking whether the product exists, which is safe because the gateway only calls internal lookups after another subgraph has already confirmed the entity exists.

For more on public vs. internal lookups and when to use each, see [Entities and Lookups](./entities-and-lookups.md).

## Replace Foreign Keys with Entity References

The `Shipment` type currently exposes a raw `ProductId`. To expose `shipment.product` instead, add a type extension. Create `Types/ShipmentNode.cs`:

```csharp filename="Shipping/Types/ShipmentNode.cs"
using HotChocolate.Types;

namespace Shipping;

[ObjectType<Shipment>]
public static partial class ShipmentNode
{
    [BindMember(nameof(Shipment.ProductId))]
    public static Product GetProduct([Parent] Shipment shipment)
        => new(shipment.ProductId);
}
```

`[BindMember(nameof(Shipment.ProductId))]` tells Hot Chocolate to replace the `productId` field on `Shipment` with the `product` field returned by this resolver. In the exported schema, clients see `shipment.product` (returning a full `Product`) instead of `shipment.productId` (a raw integer). The gateway resolves the full Product from whichever subgraph owns it.

# Configure the Server

Set `Program.cs` to:

```csharp
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

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

`#!csharp AddGraphQL("Shipping")` sets the subgraph name used during schema export. `AddTypes()` is source-generated and registers all types discovered by the analyzer. `RunWithGraphQLCommands(args)` enables CLI commands like schema export.

# Export the Schema

From the project root, export the schema:

```shell
dotnet run --project ./Shipping -- schema export
```

This generates two files in the Shipping directory:

- **`schema.graphqls`** contains the subgraph's GraphQL schema.
- **`schema-settings.json`** contains the subgraph settings.

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

```json
{
  "name": "Shipping",
  "transports": {
    "http": {
      "url": "http://localhost:5003/graphql"
    }
  }
}
```

The `name` field identifies this subgraph within the composite schema and must be unique. The `url` is where the gateway sends requests to this subgraph at runtime.

# Compose

Run composition with all subgraph schemas, including your new one:

```shell
nitro fusion compose \
  -s Products/schema.graphqls \
  -s Reviews/schema.graphqls \
  -s Shipping/schema.graphqls \
  -a gateway.far
```

If composition succeeds, copy the updated `gateway.far` to your gateway project directory:

```shell
cp gateway.far Gateway/gateway.far
```

If you already have a composed `gateway.far` with the Products and Reviews subgraphs, you can add the new subgraph to the existing archive:

```shell
nitro fusion compose \
  -s Shipping/schema.graphqls \
  -a gateway.far
```

# Test Cross-Subgraph Queries

Start all services and the gateway. With the Shipping subgraph added, you can now query shipment data that crosses subgraph boundaries:

```graphql
query {
  productById(id: 1) {
    name
    price
    shipments {
      trackingNumber
      status
    }
  }
}
```

This query touches two subgraphs:

1. The gateway calls the Products subgraph to fetch `name` and `price`.
2. The gateway uses the internal lookup to enter the Shipping subgraph and resolve `shipments` for the same product.

The client sees one unified response with fields from both subgraphs merged into a single `Product`.

You can also query in the other direction, starting from a shipment and navigating to the product:

```graphql
query {
  shipmentById(id: 1) {
    trackingNumber
    status
    product {
      name
      price
    }
  }
}
```

Here the gateway resolves the shipment from the Shipping subgraph, then uses the Products subgraph to fetch `name` and `price` for the referenced product.

# Troubleshooting Composition Errors

If composition fails after adding your new subgraph, the error messages point to specific issues.

## Duplicate field without sharing

**"Field X is defined in multiple subgraphs"**. Your new subgraph defines a field that already exists in another subgraph. Key fields (like `id`) are automatically shareable, but all other duplicated fields need `@shareable` on every definition. See [Field Ownership](./field-ownership-and-sharing.md) 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](./entities-and-lookups.md).

## 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](./data-requirements-and-mapping.md) for the full range of `@require` patterns and FieldSelectionMap syntax.
- **Composition failed?** See [Composition](./composition.md) for the full merging rules, common errors, and fixes.
- **Want to understand entities more deeply?** See [Entities and Lookups](./entities-and-lookups.md) for the complete guide to entity stubs, public vs. internal lookups, and composite keys.
- **Ready to deploy?** See [Deployment and CI/CD](./deployment-and-ci-cd.md) 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"
description: "Secure a Hot Chocolate Fusion gateway with ASP.NET Core authentication: JWT bearer setup, header propagation to subgraphs, and mTLS trust boundaries."
---

The Fusion gateway builds on ASP.NET Core authentication and authorization. You can use the same features you use in any ASP.NET Core service, including JWT validation, cookie authentication, OpenID Connect, and mTLS.

Fusion does not add its own authentication or authorization mechanisms. It relies on ASP.NET Core, so you keep the same configuration model, extension points, and security-hardened framework you already use.

# Authenticating clients at the gateway

The gateway authenticates incoming requests with the same APIs as any ASP.NET Core service. There is no Fusion-specific configuration; use `AddAuthentication(...)` and `UseAuthentication()` as usual.

A minimal JWT bearer setup for the gateway:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://login.example.com/";
        options.Audience = "https://api.example.com";
    });

builder.Services.AddAuthorization();

builder.Services
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far");

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapGraphQL();

app.Run();
```

Everything in the `AddJwtBearer` callback is standard ASP.NET Core. For details on JWKS rotation, multi-issuer setups, custom audience validation, refresh policies, and clock skew handling, see the [Microsoft Learn reference](https://learn.microsoft.com/aspnet/core/security/authentication/configure-jwt-bearer-authentication). You do not need to implement those features yourself.

The example above configures your service to validate JWT access tokens at the gateway. It does not reject requests when the token is missing or invalid. To require authentication, add authorization to the GraphQL endpoint.

## Requiring authentication on the GraphQL endpoint

To reject anonymous requests at the gateway before they reach the GraphQL pipeline, attach an authorization policy to the endpoint:

```csharp
app.MapGraphQL().RequireAuthorization();
```

Or apply a named policy that requires a specific scope:

```csharp
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("graphql:read", policy =>
        policy.RequireAuthenticatedUser()
              .RequireClaim("scope", "graphql:read"));
});

app.MapGraphQL().RequireAuthorization("graphql:read");
```

Without `RequireAuthorization`, anonymous requests reach the GraphQL pipeline, and any field not protected by the subgraphs is accessible without credentials. Whether this is acceptable depends on your API. For example, a public catalog with an authenticated checkout flow may leave the endpoint open and let subgraphs protect sensitive fields. An internal back-office gateway typically requires authentication at the edge.

## Cookies Authentication & Browser Clients

You can also use cookie authentication at the gateway and serve UI assets directly from it. For guidance, see [Cookie Authentication in ASP.NET Core](https://learn.microsoft.com/aspnet/core/security/authentication/cookie). If you are building a browser-based client, review [UI and SPA considerations](https://learn.microsoft.com/en-us/aspnet/core/client-side/spa/intro?view=aspnetcore-10.0).

In general, we **do NOT recommend** serving SPA applications directly from the gateway. Instead, put a Backend for Frontend (BFF) in front of the gateway. You can build your own BFF or [use Duende BFF](https://duendesoftware.com/products/bff). The BFF handles browser-specific authentication and session management, then calls the gateway with a token-based approach. This keeps the gateway focused on GraphQL traffic while the BFF manages browser authentication and session concerns.

# Forwarding identity to subgraphs

By default, the gateway does not forward headers to subgraphs. This means subgraphs do not receive the caller's access token or other identity information. Because authorization is usually handled by the subgraph, you typically need to pass the access token or relevant identity data so the subgraph can authorize requests correctly.

## Header propagation

Most often, you want to forward the `Authorization` header containing the access token to the subgraph. The subgraph can then validate the JWT and run authorization checks.

Fusion uses the official ASP.NET Core header propagation mechanism to forward headers to subgraphs:

```csharp
builder.Services.AddHeaderPropagation(options =>
{
    options.Headers.Add("Authorization");
    options.Headers.Add("GraphQL-Preflight");
});
```

See the [Microsoft Learn documentation on header propagation](https://learn.microsoft.com/aspnet/core/fundamentals/http-requests?view=aspnetcore-10.0#header-propagation) for more details.

You must configure header propagation on each HTTP client individually. Otherwise, it will not take effect. By default, Fusion uses a named HTTP client called `"fusion"` to call subgraphs. You can change this name in the `schema-settings.json` of each subgraph. If you keep the default, configure header propagation on the `"fusion"` client:

```json
{
  "transports": {
    "http": {
      "url": "http://products:5001/graphql",
      "clientName": "fusion"
    }
  }
}
```

Register header propagation for that client:

```csharp
builder.Services
    .AddHttpClient("fusion")
    .AddHeaderPropagation();
```

Add the `UseHeaderPropagation` middleware before the GraphQL middleware in your pipeline so headers are forwarded:

```csharp
// Later in the pipeline.
app.UseHeaderPropagation();

app.MapGraphQL();
```

## Authentication termination

In many enterprise scenarios, you may not want to forward the access token through the entire stack. With Fusion, you have full control over outgoing requests, so you can adapt the gateway to your authentication model and support custom authentication flows.

To intercept outgoing requests, add a `DelegatingHandler` to the HTTP client that calls subgraphs. This lets you derive headers from validated claims, exchange the incoming token for a new token with the correct audience, or call an external service to get additional caller information to forward.

```csharp
public sealed class ClaimsHeaderHandler(IHttpContextAccessor httpContextAccessor)
    : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var user = httpContextAccessor.HttpContext?.User;

        var userId = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (!string.IsNullOrEmpty(userId))
        {
            request.Headers.TryAddWithoutValidation("X-User-Id", userId);
        }

        var tenantId = user?.FindFirst("tenant_id")?.Value;
        if (!string.IsNullOrEmpty(tenantId))
        {
            request.Headers.TryAddWithoutValidation("X-Tenant-Id", tenantId);
        }

        return base.SendAsync(request, cancellationToken);
    }
}
```

Register the handler and configure the client:

```csharp
builder.Services.AddHttpContextAccessor();
builder.Services.AddTransient<ClaimsHeaderHandler>();

builder.Services
    .AddHttpClient("fusion")
    .AddHeaderPropagation()
    .AddHttpMessageHandler<ClaimsHeaderHandler>();
```

# Client-to-gateway trust (mTLS)

In some scenarios, only trusted clients should be able to call your gateway. Mutual TLS (mTLS) supports this by requiring the client to present a certificate. The gateway validates that certificate against a trusted certificate authority (CA).

Kestrel supports mTLS directly, and you can configure it with the `ConfigureKestrel` API. For details, see the [Microsoft Learn documentation](https://learn.microsoft.com/aspnet/core/security/authentication/certauth).

# 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:

- [Certificate authentication](https://learn.microsoft.com/aspnet/core/security/authentication/certauth)
- [Kestrel client certificates](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints#client-certificates)

# Authorization

Authorization 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](../hotchocolate/security/authorization.md).

---

### Website Content Docs Fusion Batching (`website/content/docs/fusion/batching.md`)

---
title: "Batching"
description: "How the Fusion gateway collapses many subgraph calls into fewer round trips with variable batching, request batching, and alias batching."
---

One wave of a query plan can produce many calls to the same subgraph: one entity lookup per item of a list, or several distinct lookups that all target the same service. Batching collapses those calls into fewer round trips, without changing the query plan.

# Where Batching Applies

Consider a query that reads a list of books from one subgraph and each book's rating from another:

```graphql
{
  books {
    title
    rating
  }
}
```

The gateway fetches the books from the first subgraph, then needs `rating` for every book from the second. With two books that is two lookups, with two hundred books it is two hundred. Batching sends them as one request instead; the same lookups still happen, they just travel together.

# The Three Batching Capabilities

Fusion knows three ways to put more than one call into one HTTP request. Each one is declared as a capability of the subgraph it is used against.

## Variable Batching

Variable batching sends **one operation with many sets of variables** in one request. The `variables` property carries an array instead of an object, and the subgraph runs the operation once per set:

```json
{
  "query": "query($id: ID!) { bookById(id: $id) { rating } }",
  "variables": [{ "id": "1" }, { "id": "2" }]
}
```

Variable batching is a Hot Chocolate protocol extension, not part of the GraphQL over HTTP specification, so only subgraphs that implement it can be sent this shape.

## Request Batching

Request batching sends **an array of independent requests** in one HTTP request. Each element is a complete GraphQL request with its own query and its own variables, so a batch can carry different operations:

```json
[
  {
    "query": "query($id: ID!) { bookById(id: $id) { rating } }",
    "variables": { "id": "1" }
  },
  {
    "query": "query($id: ID!) { authorById(id: $id) { name } }",
    "variables": { "id": "7" }
  }
]
```

When the subgraph supports request batching but not variable batching, the gateway flattens each operation's variable sets into one array element per set, so the batch still travels as a single request.

Request batching is also a protocol extension. Subgraphs that do not accept a JSON array as the request body cannot be sent this shape.

## Alias Batching

Alias batching needs no protocol extension. The gateway merges the calls into **one plain GraphQL operation** whose root selections are aliased per item, which any spec-compliant GraphQL server can answer.

For the two book lookups above, the gateway sends:

```graphql
query Op_68b774cc_Batch_2963222070b1fe07(
  $_0___fusion_1_id: ID!
  $_1___fusion_1_id: ID!
) {
  _0_bookById: bookById(id: $_0___fusion_1_id) {
    ..._fusion_body_1
  }
  _1_bookById: bookById(id: $_1___fusion_1_id) {
    ..._fusion_body_1
  }
}

fragment _fusion_body_1 on Book {
  rating
}
```

```json
{
  "_0___fusion_1_id": "1",
  "_1___fusion_1_id": "2"
}
```

The subgraph answers with one response, and the gateway splits it by alias:

```json
{
  "data": {
    "_0_bookById": { "rating": "1" },
    "_1_bookById": { "rating": "2" }
  }
}
```

The merged operation follows three rules:

- **Each item gets a prefix.** The root selection is aliased with `_<index>_`, and the variables that vary per item are renamed with the same prefix.
- **Shared variables are declared once.** A variable the gateway forwards from the client request carries one value for the whole batch, so it keeps its name and appears once in the variable definitions.
- **Repeated bodies become one fragment.** When several items select the same body, the body is emitted once as a generated fragment and each item spreads it.

Only query operations are merged. Mutations and subscriptions are never merged, and a request that uploads files keeps its own multipart round trip while the remaining requests still share one.

# How Fusion Chooses a Protocol

The gateway decides per subgraph, from the capabilities that subgraph declares. There are two independent dimensions.

**Many variable sets of one operation.** Variable batching wins whenever the subgraph declares it. Alias batching serves this dimension when variable batching is absent.

**Distinct operations in one wave.** Request batching wins whenever the subgraph declares it. Alias batching serves this dimension when request batching is absent. When neither is available, the gateway sends one HTTP request per operation.

The two decisions are taken independently, so a subgraph that declares only one of the two protocol extensions uses it for its own dimension and alias batching for the other. A batch that would merge fewer than two items is sent on the subgraph's native protocol instead.

# Configuring Batching Capabilities

Batching capabilities live under `capabilities.batching` in a source schema's HTTP transport settings:

```json
{
  "name": "Products",
  "transports": {
    "http": {
      "url": "http://products/graphql",
      "capabilities": {
        "batching": {
          "variableBatching": true,
          "requestBatching": true,
          "aliasBatching": true
        }
      }
    }
  }
}
```

All three flags are optional and independent. A flag you declare always wins; a flag you omit keeps its default.

## Defaults

The default depends on how the source schema is connected:

| Source schema                             | `variableBatching` | `requestBatching` | `aliasBatching` |
| ----------------------------------------- | ------------------ | ----------------- | --------------- |
| GraphQL (the default connector)           | on                 | on                | off             |
| Apollo Federation (see the section below) | off                | off               | on              |

The default GraphQL connector assumes a subgraph that implements both protocol extensions, which a Hot Chocolate source schema does. An Apollo Federation subgraph is only assumed to speak plain GraphQL.

The connector kind only sets the starting value, so partial settings mix with it per flag:

- `#!json { "requestBatching": true }` on an Apollo Federation subgraph yields request batching **and** alias batching, and request batching is then preferred for distinct operations.
- `#!json { "aliasBatching": false }` on an Apollo Federation subgraph yields no batching at all.
- `#!json { "variableBatching": false, "requestBatching": false }` on an Apollo Federation subgraph still leaves alias batching on. Declare `"aliasBatching": false` as well to turn batching off completely.

> [!NOTE]
> The settings template a Hot Chocolate subgraph exports declares all three flags explicitly, so a gateway that uses it does not fall back to the defaults. `variableBatching` and `requestBatching` mirror the batching the subgraph's server options allow, which `AddSourceSchemaDefaults()` turns on, and `aliasBatching` is always `true`.

# Apollo Federation Subgraphs

An Apollo Federation subgraph is entered through the `_entities` field, and the entity keys travel in its `representations` argument. One `_entities` operation therefore already resolves many entities with a single variable set, so variable batching never applies on this path.

That leaves distinct operations: several `_entities` calls with different sub-selections, or several different lookups against the same subgraph in one wave. For those, the gateway uses request batching if the subgraph declares it, alias batching otherwise, and one request per operation when neither is available.

By default only alias batching is on, so two `_entities` calls in one wave travel as one merged operation:

```graphql
query Op_88f73202_Batch_d9cfb37825dbe30d(
  $_0_representations: [_Any!]!
  $_1_representations: [_Any!]!
) {
  _0__entities: _entities(representations: $_0_representations) {
    ... on Child {
      b: value(suffix: "!")
    }
  }
  _1__entities: _entities(representations: $_1_representations) {
    ... on Child {
      a: value
    }
  }
}
```

See the [Apollo Federation Connector](./connectors/apollofederation.md#batching) page for the connector-specific settings.

# Errors in a Batched Request

A merged operation produces a single response, and the gateway attributes its errors by path: the first segment of an error path is the alias of the item, so the error is routed to that item and rewritten to the item's own field name. An error without a path applies to the whole request and is handed to every item of the batch.

The result is the same as if the calls had been sent separately. In the two-book example, an error on the second book's `rating` appears on that book alone:

```json
{
  "data": {
    "books": [
      { "title": "C# in Depth", "rating": "1" },
      { "title": "The Lord of the Rings", "rating": null }
    ]
  },
  "errors": [
    {
      "message": "Rating unavailable.",
      "path": ["books", 1, "rating"]
    }
  ]
}
```

Independent of batching, the `capabilities.onError` setting selects the error handling mode the gateway asks a subgraph for (`"propagate"` or `"null"`). It is not set by default, so the gateway sends no `onError` value and the subgraph applies its own behavior.

# Next Steps

- **"I want to tune the transport itself"**: [Performance Tuning](./performance-tuning.md) covers the named `HttpClient`, HTTP/2, request deduplication, and concurrency limits.
- **"I want to understand where the lookups come from"**: [Entities and Lookups](./entities-and-lookups.md) explains how the gateway enters a subgraph by key.
- **"I run Apollo Federation subgraphs"**: [Apollo Federation Connector](./connectors/apollofederation.md) covers the connector end to end.

---

### Website Content Docs Fusion Cache Control (`website/content/docs/fusion/cache-control.md`)

---
title: "Cache Control"
description: "Understand HTTP Cache-Control and Vary headers, then learn how Fusion uses GraphQL @cacheControl directives to generate safe CDN and browser caching policies."
---

Cache-Control is the HTTP header field that tells browsers, reverse proxies, and CDNs how they are allowed to store and reuse a response instead of sending the same request back to the server every time. Together with related headers such as `Vary`, it makes cached responses safe and predictable by defining whether a response may be reused, how long it may be reused, and which parts of the request affect that decision.

# Why Cache Control Matters

Good cache rules help you:

- Reduce latency for users.
- Reduce load on your backend services.
- Keep cache behavior predictable.
- Protect user-specific data from shared caches.

To make caching work safely and predictably, you need two things:

1. A deterministic GET route so caches get a stable cache key.
2. Correct Cache-Control (and Vary) headers so caches know where and how long a response may be reused.

# Why GraphQL Needs Extra Care

GraphQL usually exposes one endpoint, but each request can ask for different fields. Two requests to the same URL can therefore return very different response shapes and different data sensitivity.

A single GraphQL response can also mix public data and user-specific data. Since HTTP cache headers apply to the full response, the gateway has to compute one safe final policy that represents everything selected in that operation.

That means one response can include:

- Public data (safe for shared caches), and
- User-specific data (not safe for shared caches).

The GraphQL operation type matters. Query operations are side-effect free reads, so they are the primary target for HTTP and CDN caching. Mutations change data and should not be cached as shared HTTP responses. Subscriptions are long-running streams and are not HTTP-cacheable.

# Deterministic GET Routes

The GraphQL over HTTP specification allows query operations to be sent over HTTP GET.

```http
GET /graphql?query=query GetProducts{products{nodes{name}}}
```

The same applies when variables are included.

```http
GET /graphql?query=query GetProducts($first:Int!){products(first:$first){nodes{name}}}&variables={"first":5}
```

In real requests, these values are URL-encoded, and for larger operations the query string quickly becomes difficult to work with. This is where persisted operations help.

## Persisted Operation Routes

In large first-party GraphQL APIs, a common approach used by companies such as Netflix, Meta, and X is to rely on trusted documents. Client operations are stored in an operation store, and clients send a stable operation identifier instead of the full query text.

> You can read more in the [First-Party API guide](./guides/first-party-api.md).

With trusted documents in place, persisted-operation routes become short and stable.

```http
GET /graphql/persisted/GetProducts/123456789
```

Variables can then be sent as query parameters.

```http
GET /graphql/persisted/GetProducts/123456789?variables={"first":5}
```

In order to use persisted-operation routes you need to add the middleware `MapGraphQLPersistedOperations`.

```csharp
var app = builder.Build();

app.MapGraphQLPersistedOperations();
```

# `@cacheControl` in GraphQL

A deterministic route alone is not enough. The gateway also needs policy metadata to decide whether a response is public or private, and how long it may be reused.

GraphQL provides the `@cacheControl` directive for this purpose. You can place it on fields and types to describe cache intent.

```graphql
type Query {
  productById(id: ID!): Product @cacheControl(maxAge: 300, sharedMaxAge: 900)

  me: UserProfile
    @cacheControl(maxAge: 60, scope: PRIVATE, vary: ["Authorization"])
}
```

# How Fusion Assembles the Final Headers

Fusion computes one effective response policy by traversing the planned operation tree. It starts at the selected root fields, reads `@cacheControl` metadata on each field, falls back to the field return type when values are missing, and continues recursively through child selections, including interfaces and unions.

All collected constraints are merged into one final policy. The merge is conservative: `max-age` and `s-maxage` take the lowest value, scope resolves to the strictest value (`private` over `public`), and `vary` values are merged, normalized, and deduplicated. Every GraphQL response already lists `Accept` in `Vary`, and the merged `vary` names are added to it. Some CDNs, Akamai by default, do not cache a response whose `Vary` lists anything other than `Accept-Encoding`. Configure the CDN to cache it anyway, or remove `Accept` from `Vary` in a [custom formatter](../hotchocolate/server/http-transport.md#adding-selecting-headers-to-vary).

Fusion computes these cache constraints for query operations. Mutation requests, Subscription request, introspection requests, and operations with no cache constraints do not get cache-control headers.

# Cache Policy Starts in Subgraphs

In Fusion, cache policy starts where data is resolved, which means your subgraphs define cache intent for their own fields.

```graphql
type Product {
  id: ID!
  name: String!
}

type UserProfile {
  id: ID!
  email: String!
}

type Query {
  productById(id: ID!): Product @cacheControl(maxAge: 300, sharedMaxAge: 900)

  me: UserProfile
    @cacheControl(maxAge: 60, scope: PRIVATE, vary: ["Authorization"])
}
```

If your subgraph runs on Hot Chocolate, you can express the `@cacheControl` directive with the `[CacheControl]` attribute.

```csharp
using HotChocolate.Caching;

[QueryType]
public static class Query
{
    [CacheControl(300, SharedMaxAge = 900)]
    public static Product? GetProductById(int id)
        => ProductRepository.GetById(id);

    [CacheControl(60, Scope = CacheControlScope.Private, Vary = ["Authorization"])]
    public static UserProfile GetMe()
        => UserProfileRepository.GetCurrent();
}
```

Enable cache-control metadata on the subgraph:

```csharp
builder
    .AddGraphQL()
    .AddCacheControl();
```

# Enable Cache Control in the Fusion Gateway

The gateway must be configured to read cache-control metadata during planning and to write the final HTTP headers on the response.

```csharp
var builder = WebApplication.CreateBuilder(args);

builder
    .AddGraphQLGateway()
    .AddCacheControl()
    .UseQueryCache();
```

`AddCacheControl()` enables cache-constraint planning. `UseQueryCache()` writes the final `Cache-Control` and `Vary` headers to HTTP responses.

# Skip Cache Control for a Specific Request

Use `SkipQueryCaching()` on `OperationRequestBuilder` to bypass cache-control for a specific request.

```csharp
using HotChocolate.AspNetCore;
using HotChocolate.Execution;

public sealed class NoCacheHeaderInterceptor : DefaultHttpRequestInterceptor
{
    public override ValueTask OnCreateAsync(
        HttpContext context,
        IRequestExecutor requestExecutor,
        OperationRequestBuilder requestBuilder,
        CancellationToken cancellationToken)
    {
        if (context.Request.Headers.ContainsKey("X-Skip-Cache-Control"))
        {
            requestBuilder.SkipQueryCaching();
        }

        return base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken);
    }
}
```

# Putting It Together

In day-to-day terms, the flow is simple:

- Subgraphs declare cache intent.
- Fusion composes that metadata.
- The gateway calculates one safe policy for each query result.
- HTTP caches enforce the resulting headers.

---

### Website Content Docs Fusion Cli (`website/content/docs/fusion/cli.md`)

---
title: "Nitro CLI Reference"
description: "Reference for the `nitro fusion` CLI commands: compose, validate, publish, download, and run Fusion gateway configurations locally or with Nitro Cloud."
---

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)

```shell
npm install @chillicream/nitro
```

The npm package bundles platform-specific native binaries. Supported platforms: linux-x64, linux-musl-x64, linux-arm64, osx-x64, osx-arm64, win-x64, win-x86, win-arm64.

## Homebrew (macOS)

```shell
brew install ChilliCream/tools/nitro-cli
```

## .NET tool

Install globally:

```shell
dotnet tool install --global ChilliCream.Nitro.CommandLine
```

Or install per-project:

```shell
dotnet new tool-manifest
dotnet tool install ChilliCream.Nitro.CommandLine
```

## Verify installation

```shell
nitro --version
```

# Commands Overview

| Command        | Purpose                                         | Cloud |
| -------------- | ----------------------------------------------- | ----- |
| `compose`      | Compose source schemas into a Fusion archive    | No    |
| `download`     | Download gateway configuration from Nitro Cloud | Yes   |
| `migrate`      | Migrate v1 to v2 configuration files            | No    |
| `publish`      | Publish a Fusion configuration to a stage       | Yes   |
| `run`          | Start a local Fusion gateway                    | No    |
| `settings set` | Configure composition settings in an archive    | No    |
| `upload`       | Upload a source schema for later composition    | Yes   |
| `validate`     | Validate a schema against a stage               | Yes   |

# Cloud Authentication Options

Commands that interact with Nitro Cloud (`download`, `publish`, `upload`, `validate`) accept these global options:

| Option            | Description                | Default                 |
| ----------------- | -------------------------- | ----------------------- |
| `--api-key <key>` | API key for authentication | `NITRO_API_KEY` env var |

If `--api-key` is not provided and no `NITRO_API_KEY` environment variable is set, the CLI uses the session from `nitro login`.

# nitro fusion compose

Composes source schemas into a Fusion archive (`.far` file).

## Syntax

```shell
nitro fusion compose [options]
```

## Options

| Option                                                                        | Description                                                                         | Default                                                      |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `--source-schema-file <path>` (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>`                                                   | URL from which to download a source schema. Can be repeated.                        | --                                                           |
| `--source-schema-settings-file <path>`                                        | Settings file for the corresponding URL occurrence. Can be repeated.                | --                                                           |
| `--archive <path>` (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 <source-local\|common-runtime-types>` | 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 <path>` (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](./connectors/apollofederation.md#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](./connectors/apollofederation.md#shareable-abstract-field-routing) for the routing behavior.

## Examples

Compose from specific files:

```shell
nitro fusion compose \
  --source-schema-file ./Products/schema.graphqls \
  --source-schema-file ./Reviews/schema.graphqls \
  --archive gateway.far \
  --environment Development \
  --enable-global-object-identification
```

Compose two remote schemas and one local schema. Each remote settings option corresponds to the URL occurrence above it:

```shell
nitro fusion compose \
  --source-schema-url https://products.example.com/graphql \
  --source-schema-settings-file ./Products/schema-settings.json \
  --source-schema-url https://reviews.example.com/graphql \
  --source-schema-settings-file ./Reviews/schema-settings.json \
  --source-schema-file ./Inventory/schema.graphqls \
  --archive gateway.far
```

After a successful composition, Nitro prints:

```text
✅ Composite schema written to '/absolute/path/to/gateway.far'.
```

Auto-discover and compose all schemas in the current directory:

```shell
nitro fusion compose --archive gateway.far
```

Watch mode for local development:

```shell
nitro fusion compose --watch
```

Watch mode observes local schema directories and paired remote settings files. A watched change triggers recomposition and fetches the remote schemas again. Nitro does not poll remote URLs.

Exclude fields tagged as experimental or internal:

```shell
nitro fusion compose \
  --exclude-by-tag experimental \
  --exclude-by-tag internal-only \
  --archive gateway.far
```

# nitro fusion download

Downloads gateway configuration from Nitro Cloud.

## Syntax

```shell
nitro fusion download [options]
```

## Options

| Option                 | Description        | Default                |
| ---------------------- | ------------------ | ---------------------- |
| `--api-id <id>`        | The API identifier | `NITRO_API_ID` env var |
| `--stage <name>`       | The stage name     | `NITRO_STAGE` env var  |
| `--output-file <path>` | Output file path   | `./gateway.far`        |

## Examples

```shell
nitro fusion download \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg== \
  --stage production \
  --output-file gateway.far
```

# nitro fusion migrate

Migrates configuration files from v1 to v2 format.

## Syntax

```shell
nitro fusion migrate <TARGET> [options]
```

## Arguments

| Argument | Values            | Description      |
| -------- | ----------------- | ---------------- |
| `TARGET` | `subgraph-config` | Migration target |

## Options

| Option                                     | Description                               | Default           |
| ------------------------------------------ | ----------------------------------------- | ----------------- |
| `--working-directory <path>` (alias: `-w`) | Directory to scan for configuration files | Current directory |

## Behavior

The `subgraph-config` target converts `subgraph-config.json` files to the `schema-settings.json` format. The migration maps `subgraph` to `name` and `http.baseAddress` to `transports.http.url`. If a `schema-settings.json` file already exists in the target directory, the migration skips that directory.

## Examples

Migrate all subgraph configs in the current directory:

```shell
nitro fusion migrate subgraph-config
```

Migrate configs in a specific directory:

```shell
nitro fusion migrate subgraph-config --working-directory ./subgraphs
```

# nitro fusion publish

Publishes a Fusion configuration to a stage on Nitro Cloud. Three input modes are available (mutually exclusive).

## Syntax

```shell
nitro fusion publish [options]
```

## Options

| Option                                         | Description                                | Default                |
| ---------------------------------------------- | ------------------------------------------ | ---------------------- |
| `--archive <path>` (alias: `-a`)               | Pre-composed Fusion archive                | --                     |
| `--source-schema <name@version>` (alias: `-s`) | Source schema identifier. Can be repeated. | --                     |
| `--source-schema-file <path>` (alias: `-f`)    | Source schema file. Can be repeated.       | --                     |
| `--api-id <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 <path>` (alias: `-w`)     | Working directory                          | Current directory      |
| `--source-metadata <json>`                     | JSON metadata about the source             | --                     |

## Input Modes

**Mode 1: Pre-composed archive.** Publish an existing `.far` file with `--archive`.

```shell
nitro fusion publish \
  --archive gateway.far \
  --tag v1.0.0 \
  --stage production \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==
```

**Mode 2: Source schema files.** Compose and publish from local files with `--source-schema-file`.

```shell
nitro fusion publish \
  --source-schema-file ./Products/schema.graphqls \
  --source-schema-file ./Reviews/schema.graphqls \
  --tag v1.0.0 \
  --stage production \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==
```

**Mode 3: Uploaded schema references.** Reference previously uploaded schemas by name and version with `--source-schema`.

```shell
nitro fusion publish \
  --source-schema products-api@v1.0.0 \
  --source-schema reviews-api@v1.0.0 \
  --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 <id>`            | The API identifier                         | `NITRO_API_ID` env var |
| `--stage <name>`           | Target stage                               | `NITRO_STAGE` env var  |
| `--tag <tag>`              | Version tag                                | `NITRO_TAG` env var    |
| `--wait-for-approval`      | Wait for manual approval before proceeding | `false`                |
| `--source-metadata <json>` | JSON metadata about the source             | --                     |

```shell
nitro fusion publish begin \
  --stage production \
  --tag v1.0.0 \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==
```

### nitro fusion publish start

Start composition for the deployment.

| Option              | Description           | Default                        |
| ------------------- | --------------------- | ------------------------------ |
| `--request-id <id>` | Deployment request ID | Uses cached value from `begin` |

```shell
nitro fusion publish start
```

### nitro fusion publish validate

Validate the configuration before committing.

| Option                           | Description                | Default                        |
| -------------------------------- | -------------------------- | ------------------------------ |
| `--request-id <id>`              | Deployment request ID      | Uses cached value from `begin` |
| `--archive <path>` (alias: `-a`) | Fusion archive to validate | --                             |

```shell
nitro fusion publish validate --archive gateway.far
```

### nitro fusion publish commit

Finalize the deployment.

| Option                           | Description              | Default                        |
| -------------------------------- | ------------------------ | ------------------------------ |
| `--request-id <id>`              | Deployment request ID    | Uses cached value from `begin` |
| `--archive <path>` (alias: `-a`) | Fusion archive to deploy | --                             |

```shell
nitro fusion publish commit --archive gateway.far
```

### nitro fusion publish cancel

Cancel the deployment.

| Option              | Description           | Default                        |
| ------------------- | --------------------- | ------------------------------ |
| `--request-id <id>` | Deployment request ID | Uses cached value from `begin` |

```shell
nitro fusion publish cancel
```

### Orchestrated Workflow

A typical advanced deployment follows this sequence:

1. `nitro fusion publish begin` -- reserve a deployment slot
2. `nitro fusion publish start` -- start composition
3. `nitro fusion publish validate` -- validate the configuration
4. `nitro fusion publish commit` -- finalize the deployment

To abort at any point, run `nitro fusion publish cancel`.

# nitro fusion run

Starts a local Fusion gateway from an archive file.

## Syntax

```shell
nitro fusion run <ARCHIVE_FILE> [options]
```

## Options

| Argument/Option                 | Description                | Default            |
| ------------------------------- | -------------------------- | ------------------ |
| `<ARCHIVE_FILE>` (required)     | Path to the Fusion archive | --                 |
| `--port <number>` (alias: `-p`) | Port to bind               | Random unused port |

The gateway auto-opens a browser with the Nitro IDE. CORS is enabled, and `GraphQL-Preflight` and `Authorization` headers are propagated.

## Examples

Start a gateway on a specific port:

```shell
nitro fusion run gateway.far --port 5000
```

Start a gateway on a random port:

```shell
nitro fusion run gateway.far
```

# nitro fusion settings set

Modifies composition settings in a Fusion archive.

## Syntax

```shell
nitro fusion settings set <SETTING_NAME> <SETTING_VALUE> [options]
```

## Options

| Option                                        | Description                                | Default                                   |
| --------------------------------------------- | ------------------------------------------ | ----------------------------------------- |
| `--archive <path>` (alias: `-a`)              | Fusion archive to modify                   | --                                        |
| `--environment <name>` (alias: `--env`, `-e`) | Environment name for variable substitution | `ASPNETCORE_ENVIRONMENT` or `Development` |

## Available Settings

| Setting                                  | Values                                   | Description                                                        |
| ---------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------ |
| `allow-non-resolvable-interface-objects` | `true`, `false`                          | Allow Apollo interface objects without a resolvable key            |
| `global-object-identification`           | `true`, `false`                          | Enable Relay-style node queries                                    |
| `include-satisfiability-paths`           | `true`, `false`                          | Include paths in satisfiability diagnostics                        |
| `node-resolution`                        | `gateway`, `source-schema`               | Choose who resolves `Query.node` IDs                               |
| `shareable-field-runtime-type-routing`   | `source-local`, `common-runtime-types`   | Choose routing for type-conditioned selections on shareable fields |
| `cache-control-merge-behavior`           | `ignore`, `include`, `include-private`   | How to merge `@cacheControl` directives                            |
| `enum-values-merge-behavior`             | `auto`, `strict`, `union`                | How to merge enum values across source schemas                     |
| `tag-merge-behavior`                     | `ignore`, `include`, `include-private`   | How to merge `@tag` directives                                     |
| `exclude-by-tag`                         | Comma-separated tags                     | Exclude fields/types by tag                                        |
| `default-list-size`                      | Non-negative integer, or `null` to unset | Assumed size for lists without `@listSize` metadata                |

## Examples

Enable source-schema node resolution on an existing archive:

```shell
nitro fusion settings set global-object-identification true --archive gateway.far
nitro fusion settings set node-resolution source-schema --archive gateway.far
```

Use common-runtime-types routing on an existing archive:

```shell
nitro fusion settings set shareable-field-runtime-type-routing common-runtime-types \
  --archive gateway.far
```

For the behavior of each routing value, see [Shareable Abstract Field Routing](./connectors/apollofederation.md#shareable-abstract-field-routing).

Configure cache control merging:

```shell
nitro fusion settings set cache-control-merge-behavior include --archive gateway.far
```

Exclude tagged fields:

```shell
nitro fusion settings set exclude-by-tag experimental,internal-only --archive gateway.far
```

# nitro fusion upload

Uploads a source schema to Nitro Cloud for later composition.

## Syntax

```shell
nitro fusion upload [options]
```

## Options

| Option                                      | Description                    | Default                |
| ------------------------------------------- | ------------------------------ | ---------------------- |
| `--api-id <id>`                             | The API identifier             | `NITRO_API_ID` env var |
| `--tag <tag>`                               | Version tag                    | `NITRO_TAG` env var    |
| `--source-schema-file <path>` (alias: `-f`) | Source schema file (required)  | --                     |
| `--working-directory <path>` (alias: `-w`)  | Working directory              | Current directory      |
| `--source-metadata <json>`                  | JSON metadata about the source | --                     |

## Examples

Upload a source schema:

```shell
nitro fusion upload \
  --source-schema-file ./src/Products/schema.graphqls \
  --tag v1.2.3 \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==
```

Upload with a git commit SHA as the tag:

```shell
nitro fusion upload \
  --source-schema-file ./schema.graphqls \
  --tag $(git rev-parse --short HEAD) \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==
```

# nitro fusion validate

Validates a schema against a stage on Nitro Cloud. Two input modes are available (mutually exclusive).

## Syntax

```shell
nitro fusion validate [options]
```

## Options

| Option                                      | Description                           | Default                |
| ------------------------------------------- | ------------------------------------- | ---------------------- |
| `--archive <path>` (alias: `-a`)            | Fusion archive to validate            | --                     |
| `--source-schema-file <path>` (alias: `-f`) | Source schema files. Can be repeated. | --                     |
| `--api-id <id>`                             | The API identifier                    | `NITRO_API_ID` env var |
| `--stage <name>`                            | Stage to validate against             | `NITRO_STAGE` env var  |

## Examples

Validate from source schema files:

```shell
nitro fusion validate \
  --source-schema-file ./Products/schema.graphqls \
  --stage production \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==
```

Validate from an archive:

```shell
nitro fusion validate \
  --archive gateway.far \
  --stage production \
  --api-id QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==
```

# Schema Settings File Reference

Each source schema requires a `schema-settings.json` file alongside its `.graphqls` schema file. This file configures the source schema name, transport settings, and per-environment variable substitutions.

```json
{
  "name": "products-api",
  "transports": {
    "http": {
      "url": "{{API_URL}}"
    }
  },
  "environments": {
    "development": {
      "API_URL": "http://localhost:5110/graphql"
    },
    "production": {
      "API_URL": "https://api.example.com/graphql"
    }
  }
}
```

## Fields

### `name`

**Type:** `string` (required)

The unique source schema name used in composition. Must match the name used in `#!csharp builder.AddGraphQL("products-api")`.

### `transports.http.url`

**Type:** `string` (required)

The URL template for the subgraph's GraphQL endpoint. Use `{{VARIABLE_NAME}}` for environment-specific substitution.

### `transports.http.devUrl`

**Type:** `string` (optional)

The URL a local development environment reaches this source schema at. It supports the same `{{VARIABLE_NAME}}` substitution as `url`.

`devUrl` is only consulted for source schemas that do not run in the local AppHost when composing for a local [Aspire](./local-development.md) run. Every other composition ignores it, including `nitro fusion compose`, `publish`, and `upload`. It never survives composition either: the composed configuration always carries a `url` only.

```json
{
  "name": "products-api",
  "transports": {
    "http": {
      "url": "https://products.example.com/graphql",
      "devUrl": "http://localhost:5110/graphql"
    }
  }
}
```

When such a source schema defines no `devUrl`, composition falls back to its `url` and logs the warning `SOURCE_SCHEMA_DEV_URL_MISSING`, because a deployed URL is often not reachable from a developer machine. When the URL that is selected for such a source schema references a variable that its environment does not define, composition logs the warning `SOURCE_SCHEMA_URL_VARIABLE_UNRESOLVED`, falls back from an unresolvable `devUrl` to the `url`, and composes an unresolvable `url` as it is. Neither case fails the composition.

### `transports.websockets.devUrl`

**Type:** `string` (optional)

The same as `transports.http.devUrl`, for the WebSocket URL of the source schema. A source schema that does not run in the local AppHost and defines no `devUrl` here keeps its configured `transports.websockets.url` without a warning.

### `transports.http.clientName`

**Type:** `string` (optional, defaults to `"fusion"`)

The named HTTP client the gateway uses to communicate with this subgraph. Must match what the gateway configures via `#!csharp builder.Services.AddHttpClient("fusion")`.

### `transports.subscriptions.transport`

**Type:** `"sse"` | `"ws"` (optional)

The transport protocol for real-time subscriptions. Defaults to `"sse"` (Server-Sent Events).

### `extensions.nitro.apiId`

**Type:** `string` (optional)

The Nitro Cloud API identifier for this subgraph. Required when using Nitro Cloud features like `upload` or `publish`.

### `environments`

**Type:** `object` (optional)

Per-environment variable substitutions. The active environment is selected via the `--environment` flag during composition or via the `ASPNETCORE_ENVIRONMENT` environment variable.

```json
{
  "environments": {
    "development": {
      "API_URL": "http://localhost:5110/graphql"
    },
    "staging": {
      "API_URL": "https://staging.example.com/graphql"
    },
    "production": {
      "API_URL": "https://api.example.com/graphql"
    }
  }
}
```

When composing with `--environment staging`, all `{{API_URL}}` placeholders resolve to the staging URL.

### `preprocessor.inferShareable`

**Type:** `boolean` (optional, defaults to `false`)

When `true`, composition automatically marks every field on this source schema that is resolvable by multiple source schemas as `@shareable`, skipping `INVALID_FIELD_SHARING` validation. Fields that are part of a `@key`, marked `@internal` or `@inaccessible`, or defined on the subscription type are not marked automatically.

# Environment Variables

The Nitro CLI reads these environment variables as defaults for their corresponding options.

| Variable                   | Corresponding Option | Used By                                    |
| -------------------------- | -------------------- | ------------------------------------------ |
| `NITRO_API_ID`             | `--api-id`           | download, publish, upload, validate        |
| `NITRO_API_KEY`            | `--api-key`          | download, publish, upload, validate        |
| `NITRO_STAGE`              | `--stage`            | download, publish, validate                |
| `NITRO_TAG`                | `--tag`              | publish, upload                            |
| `NITRO_FUSION_CONFIG_FILE` | `--archive`          | publish                                    |
| `NITRO_OUTPUT_FILE`        | `--output-file`      | download                                   |
| `NITRO_REQUEST_ID`         | `--request-id`       | publish begin/start/validate/commit/cancel |
| `ASPNETCORE_ENVIRONMENT`   | `--environment`      | compose, settings set                      |

Set these in CI/CD pipelines to avoid passing values as command-line arguments.

# Exit Codes

- `0` -- success
- Non-zero -- failure (error details printed to stderr)

---

### Website Content Docs Fusion Composition (`website/content/docs/fusion/composition.md`)

---
title: "Composition"
description: "How Fusion composition validates and merges source schemas into an execution schema, with the full pipeline phases and diagnostic log codes explained."
---

# What Is Composition?

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](./getting-started.md). The pages on [Entities and Lookups](./entities-and-lookups.md) and [Field Ownership](./field-ownership-and-sharing.md) cover the directives most often referenced by the log codes below.

# The Composition Pipeline

Composition runs as an ordered set of phases. Each phase reads the output of the previous one, runs its rules, and either passes the schema to the next phase or aborts with diagnostic errors. Composition halts at the first phase that produces errors. Within a single phase, several errors may be reported together before the pipeline stops at the phase boundary.

```mermaid
flowchart TD
    A[Source Schemas] --> B[1. Parse]
    B --> C[2. Preprocess]
    C --> D[3. Enrich]
    D --> E[4. Validate Source Schemas]
    E --> F[5. Pre-Merge Validation]
    F --> G[6. Merge]
    G --> H[7. Post-Merge Validation]
    H --> I[8. Validate Satisfiability]
    I --> J[Execution Schema]
    B -.fail.-> X[Composition Error]
    C -.fail.-> X
    D -.fail.-> X
    E -.fail.-> X
    F -.fail.-> X
    G -.fail.-> X
    H -.fail.-> X
    I -.fail.-> X
```

## 1. Parse Source Schemas

Parses each source schema into an internal representation. This phase catches syntactically invalid SDL (`INVALID_GRAPHQL`).

## 2. Preprocess Source Schemas

Normalizes each source schema for composition. This includes applying tag-based exclusions and any version-specific or interop transformations needed before the validation phases run.

## 3. Enrich Source Schemas

Decorates each schema with metadata extracted from directives, such as key fields, shareability, accessibility flags, and lookup information. The metadata is attached to types and fields so later phases can reason about them efficiently without re-parsing directive arguments.

## 4. Validate Source Schemas

Runs per-schema validation rules against each source schema independently. Each source schema is checked on its own at this stage, before any cross-schema reasoning. Examples of issues caught here include `@external` placed on an interface field, malformed `@key` selection sets, root types with non-default names, and `@override` referencing the same source schema.

## 5. Pre-Merge Validation

Validates compatibility _across_ source schemas. This is where the pipeline checks that two source schemas which both define the same type agree on its shape: enum values match, output field types are mergeable, external argument types align, and input types declare the same required fields. Most "two source schemas disagree" diagnostics are surfaced here.

## 6. Merge Source Schemas

Combines all validated source schemas into a single merged schema. Same-named types are unified, Fusion metadata is applied, and lookup information is recorded so the gateway knows which subgraph owns which field.

## 7. Post-Merge Validation

Validates the merged schema as a whole. The rules here treat the composed schema as if it were a single GraphQL schema and check global invariants: no merged type is empty, all interfaces are implemented, no field references an inaccessible or internal type, the root query type exposes accessible fields, and default values do not point at inaccessible enum values.

## 8. Validate Satisfiability

Performs reachability analysis. Starting from the root types, the pipeline walks every reachable field in the merged schema and confirms it can be resolved by at least one subgraph given the available `@lookup` and `@key` paths. If a field is reachable from a query but no subgraph can produce it, satisfiability reports `UNSATISFIABLE_QUERY_PATH`. This is normally an error. When source-schema node resolution has a usable dispatcher but does not cover a composite `Node` type, Fusion reports a warning because the source resolver can return `null` or an error at runtime.

# Cost Metadata Derivation

Fusion composition records each compatible source usage as an internal `@fusion__cost` or `@fusion__listSize` provenance entry, folds the source values, and projects the result as a public directive. A public `@cost` or `@listSize` directive is emitted only when at least one source has a compatible usage. The internal entries identify the source schema and preserve its declared values. An unannotated serving source contributes the coordinate default to the public `@cost` fold without adding an internal provenance entry.

Both folds only consider serving sources: a source schema that resolves the field itself. A source that provides the field only as partial (for example an Apollo Federation `@external` field returned through `@provides`) is not a serving source for either fold, so an unannotated partial member contributes neither the coordinate's default weight nor a gap that widens the `@listSize` bound. Its own `@cost` or `@listSize` usage, when declared, still folds in and is still recorded as its own provenance entry.

A usage without a local directive definition receives the canonical definition during composition. A locally declared definition can use a compatible subset of the canonical arguments.

For `@cost`, the public weight is the maximum effective weight across every serving source. A declared weight is effective as written. An unannotated serving source contributes the default for the coordinate:

| Coordinate                                            | Default weight |
| ----------------------------------------------------- | -------------- |
| Object type                                           | `1`            |
| Scalar or enum type                                   | `0`            |
| Output field returning an object, interface, or union | `1`            |
| Output field returning a scalar or enum               | `0`            |
| Argument or input field with an input-object value    | `1`            |
| Argument or input field with a scalar, enum, or ID    | `0`            |

For example, a composite-typed coordinate with weight `-7` in one source and no declared weight in another source derives the public weight `1`. Given these two source schemas:

```graphql
# Schema A
type Query {
  book: Book @cost(weight: "-7")
}

type Book {
  id: ID
}
```

```graphql
# Schema B
type Query {
  book: Book
}

type Book {
  id: ID
}
```

Composition folds `Query.book` to the public weight `1` and records schema A's declared value as its own provenance entry; schema B contributes the coordinate default without one:

```graphql
type Query @fusion__type(schema: A) @fusion__type(schema: B) {
  book: Book
    @cost(weight: "1")
    @fusion__cost(schema: A, weight: "-7")
    @fusion__field(schema: A)
    @fusion__field(schema: B)
}
```

The public `@listSize` directive applies these folds:

| Argument                             | Fold                                                                   |
| ------------------------------------ | ---------------------------------------------------------------------- |
| `assumedSize`                        | Maximum over sources that declare it.                                  |
| `slicingArguments` and `sizedFields` | Deterministic union in first-seen order.                               |
| `requireOneSlicingArgument`          | Apply each source definition's default when omitted, then true if any. |
| `slicingArgumentDefaultValue`        | Maximum over sources that declare it.                                  |

The name unions are preserved as declared. A name that does not match an argument or child field in the composite schema contributes no runtime value, and list-size selection continues through its remaining fallbacks.

For `assumedSize`, given these two source schemas:

```graphql
# Schema A
type Query {
  field: [Int] @listSize(assumedSize: 10)
}
```

```graphql
# Schema B
type Query {
  field: [Int] @listSize(assumedSize: 5)
}
```

Composition folds `assumedSize` to the maximum of the two, `10`, and records both declared values as provenance entries:

```graphql
type Query @fusion__type(schema: A) @fusion__type(schema: B) {
  field: [Int]
    @listSize(assumedSize: 10)
    @fusion__field(schema: A)
    @fusion__field(schema: B)
    @fusion__listSize(schema: A, assumedSize: 10)
    @fusion__listSize(schema: B, assumedSize: 5)
}
```

The `slicingArgumentDefaultValue` argument is optional. A source can use a spec-only `@listSize` definition that omits this ChilliCream extension, and it composes without a warning. If every source omits the value, the public directive omits it too.

Composition reports a normal `INVALID_GRAPHQL` error for invalid locally declared definitions. Applying an argument-less local `@cost` definition reports `The @cost directive must have a 'weight' argument of type String.`, the source schema name, and the usage coordinate.

## Default List Size

The assumed size for a list field that carries no applicable `@listSize` information is a composition setting, not a gateway runtime option. Set `SourceSchemaMergerOptions.DefaultListSize` on the composer:

```csharp
var options = new SchemaComposerOptions
{
    Merger =
    {
        DefaultListSize = 100
    }
};
```

When set, composition writes the value onto the execution schema with a schema-level `@fusion__cost_options(defaultListSize:)` directive, and folds it into the effective `assumedSize` of a field for which at least one serving source declares a `@listSize`, so the public `@listSize` reflects the higher, sound bound. For a field no source annotates, composition emits no `@listSize` and the gateway applies the value directly from `@fusion__cost_options`. When absent (the default), no `@fusion__cost_options` usage is emitted and the gateway treats an unannotated list as unbounded.

See [Cost Analysis](./cost-analysis.md) for gateway enforcement, reporting, and options.

# Common Scenarios

A few rules account for most composition failures. Knowing the shape of the error helps you spot the cause quickly.

**Two source schemas return incompatible types for the same field.** If `Product.price` is `Float` in one source schema and `Int` in another, composition fails with `OUTPUT_FIELD_TYPES_NOT_MERGEABLE`. Scalar and enum return types must match exactly, while object, interface, and union types merge when one declared type is a supertype of the others (for example, `Product` and a `#!sdl union FeaturedItem = Product` merge to `FeaturedItem`). Align the types when no common supertype exists.

**A field is defined in multiple source schemas without `@shareable`.** Defining `User.name` in both Accounts and Reviews without marking it `@shareable` produces `INVALID_FIELD_SHARING`.

```graphql
# Accounts source schema
type User @key(fields: "id") {
  id: ID!
  name: String!
}

# Reviews source schema
type User @key(fields: "id") {
  id: ID!
  name: String!
}
```

Composition emits one diagnostic per non-shareable definition, so both source schemas are reported:

> The field 'User.name' in schema 'Accounts' must be shareable.
>
> The field 'User.name' in schema 'Reviews' must be shareable.

Add `@shareable` to `User.name` in both source schemas that define it. See [Field Ownership](./field-ownership-and-sharing.md) 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](./entities-and-lookups.md) 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](https://graphql.github.io/composite-schemas-spec/draft/#sec-Disallowed-Inaccessible-Elements)                          | (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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./field-ownership-and-sharing.md).                                                                                                                                                                                                                                                                                                                                                                                                                     |
| [EXTERNAL_ON_INTERFACE](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./field-ownership-and-sharing.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `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](https://graphql.github.io/composite-schemas-spec/draft/#sec-Field-Argument-Types-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-Field-With-Missing-Required-Arguments)        | 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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./interface-objects.md#replace-a-projected-default).                                                                                                                                                                                                                                                                                                                                                                                                       |
| [INPUT_FIELD_DEFAULT_MISMATCH](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-Input-Field-Types-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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-objects.md#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-objects.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `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-objects.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `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](./interface-objects.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./field-ownership-and-sharing.md).                                                                                                                                                                                                                                                                                                                                |
| [INVALID_GRAPHQL](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./getting-started.md) 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](./connectors/apollofederation.md#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](./interface-objects.md#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](./local-development.md#composition-settings) for the Aspire setting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| [INVALID_SHAREABLE_USAGE](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./field-ownership-and-sharing.md).                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| [IS_INVALID_FIELDS](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./entities-and-lookups.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| [IS_INVALID_FIELD_TYPE](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./entities-and-lookups.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| [KEY_DIRECTIVE_IN_FIELDS_ARGUMENT](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./entities-and-lookups.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| [KEY_FIELDS_SELECT_INVALID_TYPE](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./entities-and-lookups.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| [KEY_INVALID_FIELDS_TYPE](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./entities-and-lookups.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| [NON_NULL_INPUT_FIELD_IS_INACCESSIBLE](https://graphql.github.io/composite-schemas-spec/draft/#sec-Non-Null-Input-Fields-cannot-be-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-Output-Field-Types-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./field-ownership-and-sharing.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| [OVERRIDE_ON_INTERFACE](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./field-ownership-and-sharing.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| [PROVIDES_DIRECTIVE_IN_FIELDS_ARGUMENT](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./field-ownership-and-sharing.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| [PROVIDES_FIELDS_HAS_ARGUMENTS](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./field-ownership-and-sharing.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| [PROVIDES_INVALID_FIELDS](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./entities-and-lookups.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| [REQUIRE_INVALID_FIELD_TYPE](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](./getting-started.md) for the expected schema layout.                                                                                                                                                                                                                                                                                                                                                                                                           |
| [ROOT_SUBSCRIPTION_USED](https://graphql.github.io/composite-schemas-spec/draft/#sec-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](https://graphql.github.io/composite-schemas-spec/draft/#sec-Type-Kind-Mismatch)                                             | The type '\{0\}' has a different kind in schema '\{1\}' (\{2\}) than it does in schema '\{3\}' (\{4\}).                                                                                                    | The same type name was used for different kinds (for example, an object in one source schema and an interface in another). Decide which kind is correct and update the other source schema, or rename one of the types so they no longer collide.                                                                                                                                                                                                                                                                                                                                                                                                                              |
| [UNSATISFIABLE_QUERY_PATH](https://graphql.github.io/composite-schemas-spec/draft/#sec-Unsatisfiable-Query-Path)                                 | (Message varies. Includes the unreachable field, the path the validator tried, and the lookups it considered.)                                                                                             | Some reachable field cannot be resolved through the available `@lookup` and `@key` paths. This is an error unless the source-schema node-resolution exception described under Warnings applies. See [Diagnosing UNSATISFIABLE_QUERY_PATH Diagnostics](#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](https://graphql.github.io/composite-schemas-spec/draft/#sec-Lookup-Returns-Non-Nullable-Type) | The lookup field '\{0\}' in schema '\{1\}' should return a nullable type.                                                                                                                     | Lookups should return nullable entities so the gateway can represent missing keys without throwing. Change the return type of the lookup field to nullable. See [Entities and Lookups](./entities-and-lookups.md).                                                                                                                                                                  |
| `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](./local-development.md#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](./local-development.md#what-happens-when-a-gateway-starts). |
| [SPECIFIED_BY_URL_MISMATCH](https://graphql.github.io/composite-schemas-spec/draft/#sec-SpecifiedBy-URL-Mismatch)                | The scalar type '\{0\}' has a different specified-by URL in schema '\{1\}' (\{2\}) than it does in schema '\{3\}' (\{4\}).                                                                    | The same scalar declares conflicting `@specifiedBy(url: ...)` URLs in different source schemas. Align the URL across all source schemas that define the scalar so the composed schema points to a single specification.                                                                                                                                                             |
| [UNSATISFIABLE_QUERY_PATH](https://graphql.github.io/composite-schemas-spec/draft/#sec-Unsatisfiable-Query-Path)                 | A source-schema `Node` dispatcher does not cover a composite `Node` type.                                                                                                                     | In source-schema node-resolution mode, Fusion allows the source resolver to report the uncovered type as `null` or an error at runtime. Confirm that this compatibility behavior is intended.                                                                                                                                                                                       |

## Diagnosing UNSATISFIABLE_QUERY_PATH Diagnostics

Most composition diagnostics point at a single source schema or definition. `UNSATISFIABLE_QUERY_PATH` is different. It is raised by the satisfiability validator (phase 8) and tells you that the merged schema as a whole has at least one reachable field that no source schema can serve through the available `@lookup` and `@key` paths. It is an error by default. Source-schema node resolution can downgrade uncovered composite `Node` types to warnings when a usable source dispatcher exists.

Because the validator weighs every option for reaching a field across every source schema, the diagnostic carries a tree of nested errors that explain why each candidate option failed. Reading that tree from the bottom up is the fastest way to find the gap.

### Anatomy of the message

The top-level message names the unreachable field:

```text
Unable to access the field 'Order.shippingAddress'.
```

Underneath it, indented with two spaces per level, are the nested errors that the validator collected for each option it tried. Common nested messages and what they signal:

| Nested message shape                                                                                                                         | What it means                                                                                                                                                                                |
| -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Unable to transition between schemas '<from>' and '<to>' for access to field '<coordinate>'.`                                               | The validator wanted to hop from one source schema to another to fetch the field, but transitioning failed. The reason for the failure appears as a further nested child under this message. |
| `No lookups found for type '<type>' in schema '<schema>'.`                                                                                   | A specific reason a transition failed: the target source schema has no `@lookup` for the entity at all.                                                                                      |
| `Unable to satisfy the requirement '<selection>' for lookup '<lookup>' in schema '<schema>'.`                                                | A `@lookup` on the target source schema needs key fields that the parent type does not expose along this path.                                                                               |
| `Unable to satisfy the requirement '<selection>' on field '<coordinate>'.`                                                                   | A field's `@require` argument depends on fields the parent type does not expose along this path.                                                                                             |
| `Type '<type>' implements the 'Node' interface, but no source schema provides a non-internal 'Query.node<Node>' lookup field for this type.` | A type implements `Node` but no source schema exposes a `#!sdl node(id: ID!)` lookup that returns it.                                                                                        |

Indentation is meaningful: a nested message is the reason its parent option failed. Start at the leaves and work up.

### Showing the path the validator took

By default, the top-level message names the failed field but not the path the validator walked to reach it. Enable `IncludeSatisfiabilityPaths` to extend the message with the path:

```text
Unable to access the field 'Order.shippingAddress' on path 'accounts:Query.viewer<User> -> accounts:User.orders<Order>'.
```

Each path segment has the shape `<schemaName>:<TypeName>.<fieldName><<FieldTypeName>>`, and segments are joined with `->`. The form is verbose, but unambiguous: it tells you which source schema served each hop and which field type the validator ended up holding.

Set the option via the CLI flag during composition:

```shell
nitro fusion compose --include-satisfiability-paths
```

Paths add noise to short outputs and pay off on any non-trivial graph. Turn them on whenever an `UNSATISFIABLE_QUERY_PATH` error is hard to triage.

### Common causes and fixes

- **Missing `@lookup` for an entity in the target source schema.** A field on schema A returns an entity that is also defined in schema B, but B has no lookup that takes the entity's key. Add a lookup on B (typically `<entity>(id: ID!): <Entity>`) annotated with `@lookup`. See [Entities and Lookups](./entities-and-lookups.md).
- **Mismatched `@key` selections across source schemas.** The shared entity has different `@key` selection sets in different source schemas, so the validator cannot find a key both sides recognize. Align the key selection sets, or add additional `@key` directives so every key the producer uses is also a key the consumer recognizes.
- **`@require` reaches for a field that is not on the path.** A `@require(field: "...")` references parent fields that the validator cannot resolve given the path it took. Either expose the required field on the parent type along that path, or rewrite the requirement.
- **`Node` interface without a node lookup.** A type implements `Node` but no source schema exposes a `#!sdl node(id: ID!): Node` lookup that returns it. Add one in any source schema that owns the entity.

When you fix the root cause, both the top-level `UNSATISFIABLE_QUERY_PATH` diagnostic and its nested children disappear together.

### Temporarily ignoring a non-accessible field

When a field is intentionally non-accessible at the moment, for example during an incremental rollout, you can ask the validator to skip it instead of failing composition. Add the qualified field name and the path to skip under the source schema's `satisfiability.ignoredNonAccessibleFields` in its `<schema>-settings.json` file:

```json
{
  "satisfiability": {
    "ignoredNonAccessibleFields": {
      "Order.shippingAddress": [
        "accounts:Query.viewer<User> -> accounts:User.orders<Order>"
      ]
    }
  }
}
```

The key is `<TypeName>.<fieldName>`. Each value is a list of paths (matching exactly what `IncludeSatisfiabilityPaths` would print for that field) where the validator should skip the check. The field name and at least one of its paths must match for the error to be silenced.

This setting lives in the per-source-schema settings file (e.g. `accounts-settings.json`), not in the archive-level composition settings.

> [!WARNING]
> Use this sparingly. It silences a real diagnostic without fixing the underlying gap, so leaving entries in place lets unsatisfiable shapes ship.

---

### Website Content Docs Fusion Cost Analysis (`website/content/docs/fusion/cost-analysis.md`)

---
title: Cost Analysis
description: "Configure Fusion cost analysis, limits, reporting, and pre-plan enforcement."
---

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](./composition.md#cost-metadata-derivation).

# How Cost Is Calculated

The analyzer produces these metrics:

- **Field cost** represents resolver and input-processing work.
- **Type cost** represents the weighted objects in the response.
- **Maximum response size** represents the maximum number of response-object fields when `MaxResponseSize` is enabled.

Weights come from the public `@cost` directive that composition derives for each coordinate. An unannotated coordinate falls back to its default weight; see [Cost Metadata Derivation](./composition.md#cost-metadata-derivation) for the default per coordinate kind.

## Field Cost Example

Walk a query for a `book` field weighted `10`, returning a `title` and an `author`:

```graphql
{
  book {
    # 10 (weight: "10")
    title # 0 (scalar, unannotated)
    author {
      # 1 (object field, unannotated)
      name # 0 (scalar, unannotated)
    }
  }
}
# Field cost: 10 + 0 + 1 + 0 = 11
```

Add pagination and the weight of every field below a sized field multiplies by the list size. This `books` field is weighted `10` and carries `#!sdl @listSize(assumedSize: 50, slicingArguments: ["first", "last"], slicingArgumentDefaultValue: 10, sizedFields: ["edges", "nodes"])`, evaluated with `first: 50`:

```graphql
{
  books(first: 50) {
    # 10 (weight: "10")
    edges {
      # 1 (object field, paid once)
      node {
        # 1 x 50 (object field, once per edge)
        title # 0 x 50 (scalar)
        author {
          # 1 x 50 (object field, once per node)
          name # 0 x 50 (scalar)
        }
      }
    }
  }
}
# Field cost: 10 + 1 + 1 x 50 + 0 x 50 + 1 x 50 + 0 x 50 = 111
```

## Type Cost Example

Type cost counts the weighted objects the response instantiates. The same paginated query instantiates one `BooksConnection`, fifty `BooksEdge` objects, fifty `Book` objects, and fifty `Author` objects, on top of the root `Query` object:

```graphql
{
  # 1 Query
  books(first: 50) {
    # 1 BooksConnection
    edges {
      # 50 BooksEdges
      node {
        # 50 Books
        title
        author {
          # 50 Authors
          name
        }
      }
    }
  }
}
# Type cost: 1 + 1 + 50 + 50 + 50 = 152
```

The examples use this source schema:

```graphql
directive @cost(
  weight: String!
) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR
directive @listSize(
  assumedSize: Int
  slicingArguments: [String!]
  sizedFields: [String!]
  requireOneSlicingArgument: Boolean = true
  slicingArgumentDefaultValue: Int
) on FIELD_DEFINITION

type Query {
  book: Book @cost(weight: "10")
  books(first: Int, last: Int): BooksConnection
    @cost(weight: "10")
    @listSize(
      assumedSize: 50
      slicingArguments: ["first", "last"]
      slicingArgumentDefaultValue: 10
      sizedFields: ["edges", "nodes"]
    )
}

type Book {
  title: String
  author: Author
}

type Author {
  name: String
}

type BooksConnection {
  edges: [BooksEdge]
  nodes: [Book]
}

type BooksEdge {
  node: Book
}
```

## Pricing Rules

- Complementary `@include` and `@skip` branches are mutually exclusive.
- Fields are collected by response name before signed weights are applied, and clamping to zero happens after the field-call sum and the per-instance type sum are calculated.
- An interface or union return weight is the signed maximum of its member object-type weights.
- A field selected through an interface is priced through each possible object type's field metadata.
- Costs on arguments of directives used in the operation contribute to field cost.
- Negative slicing-argument values clamp to `0`. A slicing value of `0` remains `0`, and the field-call cost is still paid once.

# List Size

The analyzer selects the assumed size for a list field in this order:

1. An inherited size from a parent `@listSize(sizedFields:)` annotation. The inherited size takes priority over the child field's own `@listSize`.
2. The maximum slicing-argument value present after coercion, including a schema argument default.
3. `slicingArgumentDefaultValue`, only when no slicing argument is present.
4. `assumedSize`.
5. The default list size configured at composition time. Without a composed default, the list is unbounded.

An explicit `null` slicing argument is not a slicing value and suppresses that argument's schema default. An undefined slicing variable behaves as an absent argument, so a schema default can apply before the remaining fallbacks. See [Default List Size](./composition.md#default-list-size) for configuring the composed default.

# Default Enforcement

`AddGraphQLGatewayServer()` enables cost enforcement in every hosting environment with these limits:

- Maximum field cost: `1,000`
- Maximum type cost: `10,000`

This environment-independent behavior differs from the default introspection rule, which permits introspection in Development.

Passing `disableDefaultSecurity: true` disables cost enforcement. Cost analysis and the `GraphQL-Cost` reporting modes remain available.

```csharp
builder.Services.AddGraphQLGatewayServer(
    disableDefaultSecurity: true);
```

Set `EnforceCostLimits` to `false` when only cost enforcement should be disabled:

```csharp
builder.Services
    .AddGraphQLGatewayServer()
    .ModifyCostOptions(options => options.EnforceCostLimits = false);
```

Set `SkipAnalyzer` to `true` to bypass analysis, enforcement, and reporting.

# Reporting and Validation

Send the `GraphQL-Cost` HTTP request header to obtain cost metrics:

| Header value | Execution | Reported value                             |
| ------------ | --------- | ------------------------------------------ |
| `report`     | Yes       | Evaluated cost for the supplied variables. |
| `validate`   | No        | Evaluated cost for the supplied variables. |

`validate` requires the variables the operation declares, exactly like `execute` and `report`. Without them, the request fails with the ordinary variable coercion error; the assumed bound is not exposed through the request pipeline. With variables, `validate` returns an extensions-only response with HTTP status `200`, including when the reported values exceed configured limits. A variable batch returns one extensions-only result per variable set, with that set's `operationCost`. A successful variable batch in `report` mode also includes one `operationCost` per result.

```json
{
  "extensions": {
    "operationCost": {
      "fieldCost": 11,
      "typeCost": 5
    }
  }
}
```

When `MaxResponseSize` is enabled, `operationCost` also contains `maxResponseSize`. Positive infinite values are serialized as the string `"Infinity"`.

# Rejections and HTTP Status

Cost-limit failures use error code `HC0047`. The error extensions identify the failed limit:

| Limit                 | Error extension keys                        |
| --------------------- | ------------------------------------------- |
| Field cost            | `fieldCost`, `maxFieldCost`                 |
| Type cost             | `typeCost`, `maxTypeCost`                   |
| Maximum response size | `maxResponseSize`, `maxAllowedResponseSize` |

```json
{
  "errors": [
    {
      "message": "The maximum allowed type cost was exceeded.",
      "extensions": {
        "code": "HC0047",
        "typeCost": 2,
        "maxTypeCost": 1
      }
    }
  ]
}
```

The HTTP status depends on the accepted response media type:

| `Accept` media type                 | Rejection status |
| ----------------------------------- | ---------------- |
| `application/graphql-response+json` | `400`            |
| `application/json`                  | `200`            |

In `report` mode, a rejected response also contains `extensions.operationCost`. For a rejected variable batch, the single rejection result contains one `operationCost`: its `fieldCost` and `typeCost` are the sums across all variable sets, and it contains `maxResponseSize` only for a response-size rejection, using the first violating set's value.

One request is one invocation of the request pipeline, and a variable batch is one request. Cost enforcement sums the field cost and type cost across all variable sets and compares those sums with `MaxFieldCost` and `MaxTypeCost`. If either sum exceeds its limit, the whole request is rejected before any variable set executes, with one `HC0047` result and the HTTP status shown above. Maximum response size remains enforced per variable set. `MaxResponseSize` is checked per variable set, not summed. If any set exceeds `MaxResponseSize`, the whole request is likewise rejected before any set executes, with one `HC0047` result. When multiple sets violate this limit, the first violating set determines the reported `maxResponseSize`.

Request batching is an array of independent requests in one HTTP request. Cost limits currently apply separately to each independent request in a request batch. Summing costs across an entire request batch is planned, with no target version.

# Pipeline Placement

The predefined Fusion pipelines run these stages in order:

1. Document Cache
2. Document Parser
3. Document Validation
4. Operation Variable Coercion
5. Cost Analysis
6. Operation Plan Cache
7. Operation Plan
8. Skip Warmup Execution
9. Concurrency Gate
10. Operation Execution

Cost enforcement runs before the operation-plan cache and before operation planning. A rejected request never enters the planner's single-flight coalescing and does not create an operation-plan cache entry or an in-flight planning entry. Document normalization, i.e. expanding fragments and removing statically excluded selections, is not a pipeline stage; it is resolved once per request by the first stage that needs it, which is variable coercion, cached by operation id and rewritten only on a cache miss, and cost analysis and operation planning then read it from the operation document info instead of re-normalizing it. Cost enforcement is per request and reflects the coerced variable values; limits on operation structure, such as maximum depth, node count, and parser limits, remain the protection against operations that are expensive to plan regardless of their variables.

A custom pipeline must place `UseOperationVariableCoercion()` before `UseCostAnalysis()`, and place `UseCostAnalysis()` before `UseOperationPlanCache()`.

# Options Reference

Configure `FusionCostOptions` with `ModifyCostOptions`:

| Option                       | Type                          | Default  | Contract                                                                                                                                       |
| ---------------------------- | ----------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `MaxFieldCost`               | `double`                      | `1,000`  | Maximum allowed field cost. Valid range: non-negative finite values or `Infinity`.                                                             |
| `MaxTypeCost`                | `double`                      | `10,000` | Maximum allowed type cost. Valid range: non-negative finite values or `Infinity`.                                                              |
| `EnforceCostLimits`          | `bool`                        | `true`   | Enforces the field, type, and response-size limits.                                                                                            |
| `SkipAnalyzer`               | `bool`                        | `false`  | Skips analysis, enforcement, and reporting.                                                                                                    |
| `MaxResponseSize`            | `double?`                     | `null`   | Maximum estimated response-field count. `null` disables this check and metric. Valid range: `null`, non-negative finite values, or `Infinity`. |
| `CostPlanCacheSize`          | `int`                         | `256`    | Maximum compiled cost plans cached per schema.                                                                                                 |
| `CaseBudget`                 | `int?`                        | `null`   | Exact cases compiled per operation before `CaseBudgetExceededBehavior` decides the fallback. `null` uses the default (510).                    |
| `CaseBudgetExceededBehavior` | `CaseBudgetExceededBehavior?` | `null`   | Behavior once compiling one operation exhausts `CaseBudget`. `null` uses the default (`EvaluatePerRequest`).                                   |

```csharp
builder.Services
    .AddGraphQLGatewayServer()
    .ModifyCostOptions(options =>
    {
        options.MaxFieldCost = 5_000;
        options.MaxTypeCost = 5_000;
        options.MaxResponseSize = 10_000;
        options.CostPlanCacheSize = 512;
    });
```

An operation whose exact compile would exceed `CaseBudget` is, by default (`CaseBudgetExceededBehavior.EvaluatePerRequest`), evaluated exactly per request instead of from a compiled plan: slower for that operation, but never over-estimated. `CaseBudgetExceededBehavior.Overestimate` restores the compiled, sound over-estimate for the unaffordable remainder instead.

> **Note:** The default list size from [List Size](#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](./composition.md) for details.

# Per-Request Cost Options

`FusionCostOptions` apply to every request by default, but a gateway can loosen or tighten those limits for a particular request or user by attaching a `FusionRequestCostOptions` to the request. A value set on the request replaces the corresponding gateway value for that request, in either direction: a request can raise a limit above the gateway default or lower it below the gateway default. Attached request options replace all of the gateway's limits for that request, and a `null` `MaxResponseSize` on the request means that request has no response-size limit.

The typical place to do this is an `IHttpRequestInterceptor`. Its `OnCreateAsync` method runs for every HTTP request and has access to the authenticated user, letting it pick request options based on group membership before the operation executes:

```csharp
public class CostOptionsHttpRequestInterceptor : DefaultHttpRequestInterceptor
{
    public override ValueTask OnCreateAsync(HttpContext context,
        IRequestExecutor requestExecutor, OperationRequestBuilder requestBuilder,
        CancellationToken cancellationToken)
    {
        if (context.User.IsInRole("developer"))
        {
            requestBuilder.SetCostOptions(
                new FusionRequestCostOptions(
                    maxFieldCost: 5_000,
                    maxTypeCost: 5_000,
                    enforceCostLimits: true,
                    skipAnalyzer: false,
                    maxResponseSize: 50_000));
        }

        return base.OnCreateAsync(context, requestExecutor, requestBuilder,
            cancellationToken);
    }
}
```

```csharp
builder.Services
    .AddGraphQLGatewayServer()
    .ModifyCostOptions(options => options.MaxResponseSize = 10_000)
    .AddHttpRequestInterceptor<CostOptionsHttpRequestInterceptor>();
```

Here, requests from the `developer` role get a higher `MaxResponseSize` (and higher field/type cost limits) than the gateway's configured limits, while every other request keeps enforcing the gateway's configured limits.

Response-size analysis itself is an opt-in that is only ever enabled per gateway, by setting `FusionCostOptions.MaxResponseSize`. A request cannot turn the analysis on: if the gateway leaves `MaxResponseSize` unset (`null`) and a request nonetheless sets `FusionRequestCostOptions.MaxResponseSize`, the request fails fast with error code `HC0062` and the message:

> The request cost options set MaxResponseSize, but the schema does not enable the response-size analysis.

To enable the check, set `FusionCostOptions.MaxResponseSize` on the gateway first; requests may then raise or lower it as needed.

Setting `FusionRequestCostOptions.SkipAnalyzer` for a request bypasses the cost analyzer entirely for that request. A `MaxResponseSize` set on the same request is then ignored.

# Accessing the Analysis Result

`RequestContext.TryGetCostAnalysisResult` provides the compiled `CostPlan` and every estimate for the request. Read the result after the cost middleware has completed:

```csharp
builder.Services
    .AddGraphQLGatewayServer()
    .UseRequest(
        next => async context =>
        {
            await next(context);

            if (context.TryGetCostAnalysisResult(out var result))
            {
                CostPlan plan = result.Plan;
                IReadOnlyList<CostEstimate> estimates = result.Estimates;
            }
        },
        key: "ReadCostAnalysisResult",
        before: WellKnownRequestMiddleware.CostAnalyzerMiddleware);
```

Cost analysis and reporting return `HC0048` when required operation or document state is missing, when a request reaches the analyzer with zero coerced variable sets (an empty variable batch executed through `IRequestExecutor`; over HTTP, the transport refuses `variables: []` before execution), or when metrics cannot be attached to the execution-result state. This applies to `execute`, `report`, and `validate` mode alike.

# Next Steps

- [Request Limits](./request-limits.md)
- [Composition](./composition.md#cost-metadata-derivation)
- [Directive Reference](./directives-reference.md)

---

### Website Content Docs Fusion Data Requirements And Mapping (`website/content/docs/fusion/data-requirements-and-mapping.md`)

---
title: "Data Requirements"
description: "Declare cross-subgraph data dependencies in Fusion with the @require directive: required fields are validated at composition and fetched by the gateway."
---

In traditional distributed systems, dependencies between services hide beneath the surface. A service assumes another service provides certain fields, responds in a certain shape, or is available at a certain time. These assumptions are invisible: they live in code, not in contracts. You discover them when something breaks in production. A field gets renamed, a service changes its response format, or a new team removes data another team depended on without knowing.

Fusion makes these dependencies **declarative and validated at build time**. When a resolver in one subgraph needs data from another subgraph, it declares that dependency explicitly in the schema using the `@require` directive. The composition step validates that every declared dependency is satisfiable before any code reaches production: the required fields must exist, be reachable, and have compatible types. If a dependency cannot be satisfied, composition fails and tells you exactly what is missing and where.

This shifts cross-service data dependencies from hidden runtime failures to visible, validated build-time contracts.

This chapter covers the directives and attributes that make this work: `@require` for declaring data requirements and `@provides` for declaring fields that a subgraph can resolve locally alongside an entity reference. You will learn the full range of patterns and the syntax behind each directive.

# Declaring Data Dependencies

Use `@require` on a resolver argument when that argument's value must come from fields owned by other subgraphs. Instead of calling another service yourself, you declare what data you need and the gateway fetches it for you.

Here is the simplest case: a resolver in the Shipping subgraph needs the product's `weight` to calculate a shipping estimate. The Products subgraph owns `weight`, not the Shipping subgraph. With `@require`, the Shipping subgraph declares this dependency directly in its schema:

**GraphQL schema**

```graphql
# Shipping subgraph
type Product {
  id: ID!
  shippingEstimate(zip: String!, weight: Float! @require(field: "weight")): Int!
}

type Query {
  productById(id: ID!): Product @lookup @internal
}
```

The `#!sdl @require(field: "weight")` directive on the `weight` argument tells the gateway: "Before calling this resolver, fetch `weight` from whichever subgraph can provide it and pass it in as the `weight` argument."

**C# resolver**

```csharp
[ObjectType<Product>]
public static partial class ProductNode
{
    public static int GetShippingEstimate(
        [Parent] Product product,
        string zip,
        [Require] float weight)
        => ShippingCalculator.Estimate(zip, weight);
}
```

The `[Require]` attribute maps to `#!sdl @require(field: "weight")` in the exported schema. Because the C# argument name `weight` matches the entity field name, the field path is inferred automatically.

When the names differ, provide the field path explicitly:

```csharp
public static int GetShippingEstimate(
    [Parent] Product product,
    string zip,
    [Require("weight")] float packageWeight)
    => ShippingCalculator.Estimate(zip, packageWeight);
```

**Public facing composite schema (what clients see)**

```graphql
type Product {
  shippingEstimate(zip: String!): Int!
}
```

The `weight` argument is gone. Clients pass only `zip`. The gateway handles the resolution of `weight` transparently.

## How the Gateway Resolves Requirements

When a resolver declares a data requirement with `@require`, three things happen during composition and execution:

1. **Composition** reads the `@require` directive and removes the annotated argument from the composite schema. Clients never see it.
2. **Query planning** detects the dependency. The gateway plans an additional fetch to retrieve the required fields from whichever subgraph can provide it.
3. **Execution** fetches the required data first, then passes it as a resolver argument when invoking the downstream subgraph.

The resolver receives the data as if it were a normal argument. It does not know or care where the data came from. Services are not coupled to each other. The contract is between a resolver and the data it needs, not between one service and another.



## Complex Requirements with Input Objects

When a resolver needs multiple fields, you can gather them into a single input object using FieldSelectionMap syntax. This is a clean approach as it puts all the requirements for a resolver into a single argument.

**GraphQL schema**

```graphql
# Shipping subgraph
type Product {
  id: ID!
  deliveryEstimate(
    zip: String!
    dimension: ProductDimensionInput!
      @require(
        field: """
        {
          weight,
          length: dimensions.length,
          width: dimensions.width,
          height: dimensions.height
        }
        """
      )
  ): Int!
}

input ProductDimensionInput {
  weight: Float!
  length: Float!
  width: Float!
  height: Float!
}
```

The FieldSelectionMap inside `@require` tells the gateway how to populate the `ProductDimensionInput` from fields on the `Product` entity:

- `weight` maps directly because the input field name matches the entity field name.
- `length: dimensions.length` maps the input field `length` from the nested entity path `dimensions.length`. The same applies to `width` and `height`.

The fields referenced in the map do not all have to come from the same subgraph. The gateway resolves each field from whichever subgraph owns it. Your resolver declares what data it needs, not which services to call.

**C# resolver**

```csharp
[ObjectType<Product>]
public static partial class ProductNode
{
    public static int GetDeliveryEstimate(
        [Parent] Product product,
        string zip,
        [Require(
            """
            {
              weight,
              length: dimensions.length,
              width: dimensions.width,
              height: dimensions.height
            }
            """)]
        ProductDimensionInput dimension)
        => ShippingCalculator.Estimate(zip, dimension);
}
```

```csharp
public sealed class ProductDimensionInput
{
    public float Weight { get; init; }
    public float Length { get; init; }
    public float Width { get; init; }
    public float Height { get; init; }
}
```

**Public facing composite schema (what clients see)**

```graphql
type Product {
  deliveryEstimate(zip: String!): Int!
}
```

The `dimension` requirement argument is hidden from clients. Clients see only `zip`; the gateway resolves the nested fields (`weight`, `length`, `width`, `height`) automatically.

## Multiple Scalar Requirements

You can annotate multiple arguments with `@require` on the same field. Each one declares an independent data dependency. So, while you can aggregate them into a single input object like explained above you also can spread them across arguments.

**GraphQL schema**

```graphql
# Inventory subgraph
type Product {
  id: ID!
  shippingEstimate(
    zip: String!
    weight: Float! @require(field: "weight")
    price: Float! @require(field: "price")
  ): Int!
}
```

**C# resolver**

```csharp
[ObjectType<Product>]
public static partial class ProductNode
{
    public static int GetShippingEstimate(
        [Parent] Product product,
        string zip,
        [Require] float weight,
        [Require] float price)
        => weight > 500 || price > 1000
            ? ExpressShipping.Calculate(weight)
            : StandardShipping.Calculate(weight);
}
```

## Nested Field Paths

`@require` can reach into nested objects using dot notation.

**GraphQL schema**

```graphql
type Product {
  id: ID!
  taxEstimate(
    countryCode: String! @require(field: "seller.address.countryCode")
    price: Float! @require(field: "price")
  ): Float!
}
```

The gateway traverses `seller.address.countryCode` on the entity and passes the resolved value as the `countryCode` argument.

## Constant Arguments in Field Selections

Fields referenced in a `@require` or `@is` field selection map may carry constant arguments to parameterize or disambiguate the selected field. This is useful when a field offers multiple representations via an argument, and the resolver always needs one specific form.

**GraphQL schema**

```graphql
# Shipping subgraph
type Product {
  shippingCost(size: Int! @require(field: "dimension(unit: METRIC)")): Float
  dimension(unit: Unit!): Int
}
```

Here the `@require` path selects `dimension` with the constant argument `unit: METRIC`. The gateway evaluates this selection against the Products subgraph and passes the resulting value to `shippingCost`.

Argument values in a field selection map must be constant literals (no variables). They must match the field's declared argument definitions, and all required arguments must be supplied. Violations surface as `IS_INVALID_FIELDS` for `@is` and `REQUIRE_INVALID_FIELDS` for `@require`. Note that `@provides` field selections must not include arguments at all; any argument usage there is reported as `PROVIDES_FIELDS_HAS_ARGUMENTS`.

## List Aggregation Paths

When an entity field is a list, you can use bracket notation to select a field from each element. The gateway collects the selected values into a flat list.

**GraphQL schema**

```graphql
type Product {
  id: ID!
  taxEstimate(
    countryCodes: [String!]! @require(field: "seller.addresses[countryCode]")
    price: Float! @require(field: "price")
  ): Float!
}
```

The path `seller.addresses[countryCode]` means: navigate to `seller.addresses` (a list), then select `countryCode` from each element. If the seller has three addresses with country codes `"US"`, `"DE"`, and `"US"`, the resolver receives `#!json ["US", "DE", "US"]` as the `countryCodes` argument.

# Declaring Contextually Available Fields

Use `@provides` on a field that returns an entity to tell the gateway that certain subfields of that entity are available when resolved through this specific field. The subgraph does not own those fields globally, but it can provide them in this context.

## When Contextual Availability Helps

Consider a Reviews subgraph where the `author` field returns a `User` entity. The `User` type and its `username` field are owned by the Accounts subgraph. Normally the gateway would need to call the Accounts subgraph to fetch `username`. But the Reviews subgraph already has the author's username available when resolving `Review.author`. By annotating the `author` field with `#!sdl @provides(fields: "username")`, the subgraph tells the gateway: "When you resolve `author` through the `Review` entity on my subgraph, I can also give you `username`."

This is different from `@shareable`, which declares that a subgraph can always resolve a field. `@provides` is conditional: the data is only available when coming through a specific field path.

**GraphQL schema**

```graphql
# Reviews subgraph
type Review {
  id: ID!
  body: String!
  author: User @provides(fields: "username")
}

type User {
  id: ID!
  username: String! @external
}

type Query {
  reviewById(id: ID!): Review @lookup
}
```

The `#!sdl @provides(fields: "username")` on `author` tells the gateway that when it resolves `author` from the Reviews subgraph, it can also get `username` without a separate call to the Accounts subgraph.

The `@external` on `username` declares that this field is owned by another subgraph (Accounts), but the Reviews subgraph can provide it in the context of `Review.author`.

**C# resolver**

```csharp
[ObjectType<Review>]
public static partial class ReviewNode
{
    [Provides("username")]
    public static User GetAuthor(
        [Parent(requires: nameof(Review.AuthorId))] Review review)
        => new User(review.AuthorId, review.AuthorUsername);
}
```

## Providing Multiple and Nested Fields

`@provides` accepts a field selection set, so you can declare multiple fields or nested fields:

```graphql
type Review {
  product: Product @provides(fields: "name price")
}
```

```graphql
type Review {
  product: Product @provides(fields: "sku variation { size color }")
}
```

## When to Provide Fields Contextually

Use `@provides` when:

- Your subgraph stores denormalized data from another subgraph (e.g., a cached username)
- Avoiding the extra round-trip measurably improves performance
- The locally stored data is kept in sync with the owning subgraph

Do not use `@provides` as a substitute for proper entity ownership. If your subgraph is the authoritative source for a field, that field should be defined as a regular field, not as `@external` with `@provides`. Similarly, if your subgraph can resolve a field on every path, use `@shareable` instead of marking it `@external` and adding `@provides` to each field that returns the entity.

# FieldSelectionMap Syntax Reference

`@require` uses the FieldSelectionMap scalar for its `field` argument. This is a mini-language for describing how to map entity fields to argument shapes.

| Syntax           | Example                                | Meaning                                                          |
| ---------------- | -------------------------------------- | ---------------------------------------------------------------- |
| Simple path      | `"weight"`                             | Maps the `weight` field directly                                 |
| Nested path      | `"dimensions.weight"`                  | Traverses into `dimensions`, then selects `weight`               |
| Object selection | `"{ weight, height }"`                 | Selects multiple fields into an object shape                     |
| Mapped selection | `"{ w: dimensions.weight }"`           | Renames: maps entity's `dimensions.weight` to argument field `w` |
| Mixed selection  | `"{ weight, len: dimensions.length }"` | Combines direct and renamed mappings                             |
| List aggregation | `"addresses[countryCode]"`             | Selects `countryCode` from each element in the `addresses` list  |
| List projection  | `"dimensions[{ weight, height }]"`     | Selects multiple fields from each list element into an object    |

## When to Use Which Syntax

**Simple path.** Use when `@require` maps one argument to one field.

```graphql
weight: Float! @require(field: "weight")
```

**Object selection.** Use when mapping multiple entity fields into a single input object argument.

```graphql
dimension: ProductDimensionInput! @require(field: "{ weight, length, width, height }")
```

**Mapped selection.** Use when the input field names differ from the entity field names, or when you need to reach into nested fields.

```graphql
dimension: ProductDimensionInput! @require(field: "{ w: weight, l: dimensions.length }")
```

**List aggregation.** Use when you need to collect a single field from each element of a list.

```graphql
countryCodes: [String!]! @require(field: "seller.addresses[countryCode]")
```

**List projection.** Use when you need multiple fields from each element but want to drop the rest.

```graphql
dims: [ProductDimensionInput!]! @require(field: "dimensions[{ weight, height }]")
```

> For the full FieldSelectionMap grammar, see the [Composite Schemas specification](https://graphql.github.io/composite-schemas-spec/draft/#sec-Appendix-A-Specification-of-FieldSelectionMap-Scalar).

# Troubleshooting

## `REQUIRE_INVALID_FIELDS`: Referenced field does not exist

```text
Error: The @require directive on argument "weight" references field "weight"
which does not exist on type "Product".
```

The field path in `@require(field: "...")` points to a field that does not exist on the entity type after composition. Check that the field name matches exactly (GraphQL field names, not C# property names) and that the owning subgraph is included in composition.

## `PROVIDES_INVALID_FIELDS`: Invalid field selection in `@provides`

The `@provides(fields: "...")` selection references one or more fields that do not exist on the returned entity type. Verify each field path (including nested fields) against the GraphQL schema.

## `PROVIDES_FIELDS_MISSING_EXTERNAL`: Provided field must be marked `@external`

A field referenced by `@provides(fields: "...")` must be declared as `@external` in the same subgraph. Mark the provided field (and nested fields, when applicable) as `@external`, or remove it from `@provides` if this subgraph owns it globally.

## `EXTERNAL_UNUSED`: External field is not referenced

Every `@external` field must be referenced by a `@provides` directive. Remove unused `@external` declarations or add the corresponding `@provides` selection.

## Required argument still visible in composite schema

If a `@require` argument appears in the composite schema when it should not, check that:

- The `@require` directive is on the argument, not on the field
- The `field` value is a valid FieldSelectionMap (invalid syntax triggers a `REQUIRE_INVALID_SYNTAX` composition error)

# Next Steps

- **Need entity identity and lookup patterns?** See [Entities and Lookups](./entities-and-lookups.md) for the full guide to keys, public vs. internal lookups, and composite keys.
- **Need field ownership contracts?** See [Field Ownership](./field-ownership-and-sharing.md).
- **Need the directive and attribute quick reference?** See the [Attribute and Directive Reference](./directives-reference.md).

---

### Website Content Docs Fusion Deployment And Ci Cd (`website/content/docs/fusion/deployment-and-ci-cd.md`)

---
title: "Deployment and CI/CD"
description: "CI/CD pipelines for Hot Chocolate Fusion: upload source schemas with `nitro fusion`, publish configurations to Nitro stages, and hot-swap gateway archives."
---

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](../nitro/index.md) 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](../nitro/index.md) for how to create an API and copy the ID.
- An **API key** with permission to upload source schemas, publish configurations, and validate against the target stage. Store it as a CI secret (`NITRO_API_KEY`).
- A **stage** to deploy to (for example `dev`, `staging`, `production`). Stages are created in Nitro and addressed by name.

> [!NOTE]
> All `nitro fusion` subcommands fall back to the environment variables `NITRO_API_ID`, `NITRO_API_KEY`, `NITRO_STAGE`, and `NITRO_TAG` when the corresponding command-line option is omitted. Setting these at the job level keeps the individual commands compact. See the [CLI reference](./cli.md) 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](./composition.md) for what composition does) and makes the resulting archive available to all gateways subscribed to that stage.

The runtime side is configured once on the gateway:

```csharp
builder.Services.AddNitro().AddDefaults();

builder.Services
    .AddGraphQLGatewayServer()
    .ModifyNitroOptions(o =>
    {
        o.Service.ApiId = "...";
        o.Service.ApiKey = "...";
        o.Service.Stage = "production";
    });
```

> [!NOTE]
> `AddDefaults()` is generated by a source generator in the `ChilliCream.Nitro` package. It requires the `ChilliCream.Nitro.Fusion` package to be referenced in your project. If `AddDefaults()` is not available, verify your package references or use the explicit `.AddFusion()` method instead. See the [Analyzers](../nitro/analyzers.md) page for details.

With the runtime in place, every successful `nitro fusion publish` becomes the new configuration the gateway serves traffic against. You don't ship the archive yourself.

## Upload the source schema

The build job exports the subgraph's GraphQL SDL and uploads it to Nitro. The export step uses the standard Hot Chocolate schema export command:

```shell
dotnet run --project ./src/SubgraphA -- schema export --output schema.graphql
```

After the export, upload the file. The `tag` you pass here is the handle the publish step uses later to find this exact upload, so use a value that's unique per build (the commit SHA works well):

<PipelineChoiceTabs>
<PipelineChoiceTabs.GitHubAction>

```yaml
- uses: ChilliCream/nitro-fusion-upload@v16
  with:
    tag: ${{ github.sha }}
    api-id: ${{ secrets.NITRO_API_ID }}
    api-key: ${{ secrets.NITRO_API_KEY }}
    source-schema-files: |
      ./src/SubgraphA/schema.graphql
```

</PipelineChoiceTabs.GitHubAction>
<PipelineChoiceTabs.AzureDevOps>

```yaml
- task: NitroFusionUpload@16
  inputs:
    # Authentication depends on the method you configured.
    # This example uses a Nitro service connection.
    nitroServiceConnection: nitro-prod
    apiId: $(NITRO_API_ID)
    tag: $(Build.SourceVersion)
    sourceSchemaFiles: |
      ./src/SubgraphA/schema.graphql
```

</PipelineChoiceTabs.AzureDevOps>
<PipelineChoiceTabs.CLI>

```shell
nitro fusion upload \
  --tag "${{ github.sha }}" \
  --api-id "${{ secrets.NITRO_API_ID }}" \
  --api-key "${{ secrets.NITRO_API_KEY }}" \
  --source-schema-file "./src/SubgraphA/schema.graphql"
```

</PipelineChoiceTabs.CLI>
</PipelineChoiceTabs>

> [!NOTE]
> The tag you pass to `upload` must match the tag you reference from `publish`. If they drift, the publish step won't find the source schema and the deployment fails before any traffic is rerouted.

A complete build-and-deploy workflow looks like this:

```yaml
name: Deploy SubgraphA

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: "10.0.x"

      - name: Export source schema
        run: dotnet run --project ./src/SubgraphA -- schema export --output ./src/SubgraphA/schema.graphql

      - name: Upload source schema to Nitro
        uses: ChilliCream/nitro-fusion-upload@v16
        with:
          tag: ${{ github.sha }}
          api-id: ${{ secrets.NITRO_API_ID }}
          api-key: ${{ secrets.NITRO_API_KEY }}
          source-schema-files: |
            ./src/SubgraphA/schema.graphql

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy subgraph
        run: |
          # deploy your subgraph application here, e.g. push container image, kubectl apply, etc.

      - name: Publish Fusion configuration
        uses: ChilliCream/nitro-fusion-publish@v16
        with:
          tag: ${{ github.sha }}
          stage: production
          api-id: ${{ secrets.NITRO_API_ID }}
          api-key: ${{ secrets.NITRO_API_KEY }}
          source-schemas: |
            subgraph-a@${{ github.sha }}
```

## Publish the Fusion configuration

The deploy job publishes a new Fusion configuration that references the source schema uploaded in the build job. Nitro re-composes from the registered source schemas, runs validation, and makes the new archive available on the target stage.

<PipelineChoiceTabs>
<PipelineChoiceTabs.GitHubAction>

```yaml
- uses: ChilliCream/nitro-fusion-publish@v16
  with:
    tag: ${{ github.sha }}
    stage: production
    api-id: ${{ secrets.NITRO_API_ID }}
    api-key: ${{ secrets.NITRO_API_KEY }}
    source-schemas: |
      subgraph-a@${{ github.sha }}
```

</PipelineChoiceTabs.GitHubAction>
<PipelineChoiceTabs.AzureDevOps>

```yaml
- task: NitroFusionPublish@16
  inputs:
    # Authentication depends on the method you configured.
    # This example uses a Nitro service connection.
    nitroServiceConnection: nitro-prod
    apiId: $(NITRO_API_ID)
    tag: $(Build.SourceVersion)
    stage: production
    sourceSchemas: |
      subgraph-a@$(Build.SourceVersion)
```

</PipelineChoiceTabs.AzureDevOps>
<PipelineChoiceTabs.CLI>

```shell
nitro fusion publish \
  --tag "${{ github.sha }}" \
  --stage "production" \
  --api-id "${{ secrets.NITRO_API_ID }}" \
  --api-key "${{ secrets.NITRO_API_KEY }}" \
  --source-schema "subgraph-a@${{ github.sha }}"
```

</PipelineChoiceTabs.CLI>
</PipelineChoiceTabs>

The `--source-schema` value uses the `name@version` syntax. The `name` is the source schema name from the subgraph's `schema-settings.json` (the `name` field), and the `version` is the tag you passed to `nitro fusion upload` in the build job. To publish a configuration that combines this subgraph with sibling subgraphs at fixed versions, repeat the option:

```shell
nitro fusion publish \
  --tag "${{ github.sha }}" \
  --stage "production" \
  --source-schema "subgraph-a@${{ github.sha }}" \
  --source-schema "subgraph-b@v2.4.1" \
  --source-schema "subgraph-c@latest"
```

> [!NOTE]
> `nitro fusion publish` must run **after** the subgraph application has been deployed and is reachable at its production URL. Once publish succeeds, gateways subscribed to that stage start routing traffic against the new schema, and any subgraph endpoint referenced by it must already accept requests. Publishing first and deploying second produces a window where the gateway sends traffic to a URL that isn't live yet.

# Pull request validation

Before merging a change to a subgraph schema, the PR validation workflow asks Nitro to compose the proposed source schema together with the currently published versions of the other subgraphs and reports any composition errors or breaking changes against the target stage. This catches incompatibilities before they reach `main`.

<PipelineChoiceTabs>
<PipelineChoiceTabs.GitHubAction>

```yaml
- uses: ChilliCream/nitro-fusion-validate@v16
  with:
    stage: production
    api-id: ${{ secrets.NITRO_API_ID }}
    api-key: ${{ secrets.NITRO_API_KEY }}
    source-schema-files: |
      ./src/SubgraphA/schema.graphql
```

</PipelineChoiceTabs.GitHubAction>
<PipelineChoiceTabs.AzureDevOps>

```yaml
- task: NitroFusionValidate@16
  inputs:
    # Authentication depends on the method you configured.
    # This example uses a Nitro service connection.
    nitroServiceConnection: nitro-prod
    apiId: $(NITRO_API_ID)
    stage: production
    sourceSchemaFiles: |
      ./src/SubgraphA/schema.graphql
```

</PipelineChoiceTabs.AzureDevOps>
<PipelineChoiceTabs.CLI>

```shell
nitro fusion validate \
  --stage "production" \
  --api-id "${{ secrets.NITRO_API_ID }}" \
  --api-key "${{ secrets.NITRO_API_KEY }}" \
  --source-schema-file "./src/SubgraphA/schema.graphql"
```

</PipelineChoiceTabs.CLI>
</PipelineChoiceTabs>

A complete PR validation workflow:

```yaml
name: Validate SubgraphA

on:
  pull_request:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: "10.0.x"

      - name: Export source schema
        run: dotnet run --project ./src/SubgraphA -- schema export --output ./src/SubgraphA/schema.graphql

      - name: Validate against production stage
        uses: ChilliCream/nitro-fusion-validate@v16
        with:
          stage: production
          api-id: ${{ secrets.NITRO_API_ID }}
          api-key: ${{ secrets.NITRO_API_KEY }}
          source-schema-files: |
            ./src/SubgraphA/schema.graphql
```

> [!NOTE]
> Validate against the same stage you'll publish to. Validating against `dev` while you're about to publish to `production` defeats the purpose, since the source schemas registered on the two stages can differ.

# Without Nitro

Nitro is the supported path. It handles the schema registry, server-side composition, breaking-change detection, persisted operation tracking, and atomic rollout to subscribed gateways. If you can use Nitro, use it.

If you can't, you compose locally, ship the resulting archive to durable storage yourself, and load it on the gateway. The local compose step produces a `gateway.far`:

```shell
nitro fusion compose \
  --source-schema-file ./src/SubgraphA/schema.graphqls \
  --source-schema-file ./src/SubgraphB/schema.graphqls \
  --archive ./gateway.far
```

The gateway loads the archive from disk:

```csharp
builder.Services
    .AddGraphQLGatewayServer()
    .AddFileSystemConfiguration("gateway.far");
```

If the archive lives in object storage, a container registry, or a custom source, implement `IFusionConfigurationProvider` and register it through `AddConfigurationProvider`:

```csharp
builder.Services
    .AddGraphQLGatewayServer()
    .AddConfigurationProvider(sp => new CustomConfigurationProvider());
```

See the [configuration provider API](./migration/migrate-from-15-to-16.md#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](./cli.md) for every `nitro fusion` command and its options.
- [Composition](./composition.md) for what runs server-side when you publish.
- [Migrating from v15 to v16](./migration/migrate-from-15-to-16.md) if you're moving an existing gateway over.

---

### Website Content Docs Fusion Directives Reference (`website/content/docs/fusion/directives-reference.md`)

---
title: "Directive Reference"
description: "Reference for Fusion composition directives like @key, @lookup, @require, and @shareable, with SDL definitions, arguments, and composed output examples."
---

Fusion implements the [GraphQL Composite Schemas Specification](https://graphql.github.io/composite-schemas-spec/draft/). 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`](#key)                         | `OBJECT`, `INTERFACE`                     |    Yes     | Define entity identity                            |
| [`@lookup`](#lookup)                   | `FIELD_DEFINITION`                        |     No     | Mark entity lookup resolvers                      |
| [`@is`](#is)                           | `ARGUMENT_DEFINITION`                     |     No     | Map lookup arguments to entity fields             |
| [`@interfaceObject`](#interfaceobject) | `OBJECT`                                  |     No     | Contribute fields to an interface                 |
| [`@implement`](#implement)             | `FIELD_DEFINITION`                        |     No     | Replace a projected interface-object default      |
| [`@require`](#require)                 | `ARGUMENT_DEFINITION`                     |     No     | Declare cross-subgraph data dependencies          |
| [`@shareable`](#shareable)             | `OBJECT`, `FIELD_DEFINITION`              |    Yes     | Allow multiple subgraphs to define the same field |
| [`@provides`](#provides)               | `FIELD_DEFINITION`                        |     No     | Declare locally-resolvable subfields              |
| [`@external`](#external)               | `FIELD_DEFINITION`                        |     No     | Mark field as owned by another subgraph           |
| [`@override`](#override)               | `FIELD_DEFINITION`                        |     No     | Migrate field ownership between subgraphs         |
| [`@internal`](#internal)               | `OBJECT`, `FIELD_DEFINITION`              |     No     | Hide from composite schema and merge process      |
| [`@inaccessible`](#inaccessible)       | 10 locations                              |     No     | Hide from client-facing composite schema          |
| [`@eventStream`](#eventstream)         | `FIELD_DEFINITION`                        |     No     | Back a subscription field with a message broker   |
| [`@eventCursor`](#eventcursor)         | `ARGUMENT_DEFINITION`, `FIELD_DEFINITION` |     No     | Mark the resume cursor for resumable streams      |

---

# Entity Identity and Resolution

## `@key`

Designates an entity's unique key, which identifies how to uniquely reference an instance of an entity across different source schemas.

```graphql
directive @key(fields: FieldSelectionSet!) repeatable on OBJECT | INTERFACE
```

| Argument | Type                 | Description                                                                      |
| -------- | -------------------- | -------------------------------------------------------------------------------- |
| `fields` | `FieldSelectionSet!` | A field selection set that forms the unique key (e.g. `"id"` or `"tenantId id"`) |

Each `@key` directive on a type specifies one distinct unique key for that entity. Apply multiple `@key` directives to define alternative keys that the gateway can use to resolve the entity. Fields referenced in a key are implicitly shareable across subgraphs -- you do not need to add `@shareable` to key fields.

Key fields may supply constant arguments to select a specific variant of a field (for example, `#!sdl @key(fields: "id(scope: LOCAL)")`). Argument values must be constant literals (no variables), must match the field's declared argument definitions, and all required arguments must be supplied. Composition reports unknown, incompatible, or missing-required arguments as `KEY_INVALID_ARGUMENTS`.

**Example -- single key:**

```graphql
# Source schema
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
}
```

```graphql
# Composed schema
type Product {
  id: ID!
  name: String!
  price: Float!
}
```

**Example -- multiple keys:**

```graphql
# Source schema
type Product @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: String!
  name: String!
}
```

**Example -- composite key:**

```graphql
# Source schema (both fields required together)
type Product @key(fields: "id sku") {
  id: ID!
  sku: String!
  name: String!
}
```

> **In C#:** `#!csharp [EntityKey("id")]` attribute. See [Entities and Lookups](./entities-and-lookups.md).

---

## `@lookup`

Marks a field as an entity lookup resolver that the gateway uses to resolve an entity by a stable key.

```graphql
directive @lookup on FIELD_DEFINITION
```

Lookup fields provide the gateway with entry points into a subgraph for entity resolution. A source schema can define multiple lookup fields for the same entity to support resolution by different keys. Lookup fields must return a nullable type and must not return a list.

**Example:**

```graphql
# Source schema
type Query {
  productById(id: ID!): Product @lookup
  productByName(name: String!): Product @lookup
}

type Product @key(fields: "id") @key(fields: "name") {
  id: ID!
  name: String!
}
```

```graphql
# Composed schema
type Query {
  productById(id: ID!): Product
  productByName(name: String!): Product
}

type Product {
  id: ID!
  name: String!
}
```

> **In C#:** `[Lookup]` attribute. See [Entities and Lookups](./entities-and-lookups.md).

---

## `@is`

Maps a lookup argument to a field on the entity type when the argument name does not match the field name.

```graphql
directive @is(field: FieldSelectionMap!) on ARGUMENT_DEFINITION
```

| Argument | Type                 | Description                                                                   |
| -------- | -------------------- | ----------------------------------------------------------------------------- |
| `field`  | `FieldSelectionMap!` | A selection map that describes the mapping from entity fields to the argument |

When a lookup argument name matches the corresponding field on the return type, you can omit `@is`. Use `@is` when the names differ or when the mapping involves nested fields. Fields in the selection map may carry constant arguments (for example, `#!sdl @is(field: "id(scope: LOCAL)")`); argument values must be constant literals (no variables), must match the field's argument definitions, and all required arguments must be supplied. Argument errors surface as `IS_INVALID_FIELDS`.

**Example -- argument name differs from field name:**

```graphql
# Source schema
type Query {
  personById(personId: ID! @is(field: "id")): Person @lookup
}

type Person @key(fields: "id") {
  id: ID!
  name: String!
}
```

```graphql
# Composed schema
type Query {
  personById(personId: ID!): Person
}

type Person {
  id: ID!
  name: String!
}
```

**Example -- nested field reference:**

```graphql
# Source schema
type Query {
  personByAddressId(id: ID! @is(field: "address.id")): Person @lookup
}
```

> **In C#:** The `@is` mapping is inferred automatically from the argument name. When it does not match, use the field parameter convention described in [Entities and Lookups](./entities-and-lookups.md).

---

# Interface Objects

## `@interfaceObject`

Declares an object type as a stand-in for an interface with the same name in another source schema. The stand-in can contribute fields to the interface without declaring its concrete implementations.

```graphql
directive @interfaceObject on OBJECT
```

The stand-in must declare at least one `@key`, and each key must match a key on the interface entity. A source that contributes non-key fields through the stand-in must also provide a lookup returning the stand-in.

**Example:**

```graphql
# Catalog source schema
interface Media @key(fields: "id") {
  id: ID!
  title: String!
}

type Book implements Media @key(fields: "id") {
  id: ID!
  title: String!
}
```

```graphql
# Analytics source schema
type Query {
  mediaByKey(id: ID!): Media @lookup @internal
}

type Media @interfaceObject @key(fields: "id") {
  id: ID!
  views: Int!
}
```

```graphql
# Composed schema
interface Media {
  id: ID!
  title: String!
  views: Int!
}

type Book implements Media {
  id: ID!
  title: String!
  views: Int!
}
```

See [Interface Objects](./interface-objects.md) for lookup requirements, projected fields, and concrete type recovery.

---

## `@implement`

Marks a field as an explicit implementation that replaces a default projected from an interface object.

```graphql
directive @implement on FIELD_DEFINITION
```

Use `@implement` only when a concrete object or a more-specific interface stand-in declares a field that would otherwise adopt a projected default. Composition rejects an unmarked replacement and rejects `@implement` when there is no applicable default.

**Example:**

```graphql
# Catalog source schema
type Book implements Media @key(fields: "id") {
  id: ID!
  title: String!
  views: Int! @implement
}
```

```graphql
# Analytics source schema
type Media @interfaceObject @key(fields: "id") {
  id: ID!
  views: Int!
}
```

In the composed schema, Catalog owns `Book.views`. Other `Media` implementations continue to use the Analytics default.

See [Replace a Projected Default](./interface-objects.md#replace-a-projected-default) for the complete pattern.

---

# Data Requirements

## `@require`

Declares that a resolver argument needs data from fields owned by other subgraphs. The gateway resolves the required data first, then passes it to the resolver. Arguments annotated with `@require` are removed from the composed client-facing schema.

```graphql
directive @require(field: FieldSelectionMap!) on ARGUMENT_DEFINITION
```

| Argument | Type                 | Description                                                             |
| -------- | -------------------- | ----------------------------------------------------------------------- |
| `field`  | `FieldSelectionMap!` | A selection map describing which fields from the entity type are needed |

Use `@require` when a resolver in one subgraph needs data that another subgraph owns. The gateway handles the data fetching automatically. This shifts cross-service data dependencies from hidden runtime failures to validated build-time contracts. Fields in the selection map may carry constant arguments to select a specific variant (for example, `#!sdl @require(field: "dimension(unit: METRIC)")`); argument values must be constant literals (no variables), must match the field's argument definitions, and all required arguments must be supplied. Argument errors surface as `REQUIRE_INVALID_FIELDS`.

**Example -- scalar requirement:**

```graphql
# Source schema (Shipping subgraph)
type Product {
  shippingEstimate(zip: String!, weight: Float! @require(field: "weight")): Int!
}
```

```graphql
# Composed schema (weight argument removed)
type Product {
  shippingEstimate(zip: String!): Int!
}
```

**Example -- structured requirement with input type:**

```graphql
# Source schema
type Product {
  delivery(
    zip: String!
    dimension: ProductDimensionInput!
      @require(field: "{ size: dimension.size, weight: dimension.weight }")
  ): DeliveryEstimates
}
```

> **In C#:** `[Require]` attribute on method parameters. See [Data Requirements](./data-requirements-and-mapping.md).

---

# Field Ownership and Sharing

## `@shareable`

Allows multiple subgraphs to define the same field. Without `@shareable`, defining the same non-key field in two subgraphs causes a composition error.

```graphql
directive @shareable repeatable on OBJECT | FIELD_DEFINITION
```

When multiple subgraphs mark the same field as `@shareable`, they declare that the field is semantically equivalent across all definitions. The gateway is free to resolve the field from any subgraph that defines it. Apply `@shareable` to an object type to make all its fields shareable.

**Example:**

```graphql
# Source schema A (Products subgraph)
type Product @key(fields: "id") {
  id: ID!
  name: String! @shareable
  description: String!
}
```

```graphql
# Source schema B (Inventory subgraph)
type Product @key(fields: "id") {
  id: ID!
  name: String! @shareable
  inStock: Boolean!
}
```

```graphql
# Composed schema
type Product {
  id: ID!
  name: String!
  description: String!
  inStock: Boolean!
}
```

> **In C#:** `[Shareable]` attribute. See [Field Ownership](./field-ownership-and-sharing.md).

---

## `@provides`

Declares that a field returning an entity can resolve specific subfields of that entity locally, without requiring an additional call to another subgraph.

```graphql
directive @provides(fields: FieldSelectionSet!) on FIELD_DEFINITION
```

| Argument | Type                 | Description                                                                                        |
| -------- | -------------------- | -------------------------------------------------------------------------------------------------- |
| `fields` | `FieldSelectionSet!` | A field selection set describing the subfields of the returned type that this subgraph can resolve |

This is a query-planning optimization. When a client requests provided subfields through this particular field path, the gateway resolves them from the current subgraph instead of making a separate call. Fields referenced in `@provides` must be marked `@external` on the return type. Fields in a `@provides` selection must not have arguments; composition reports any argument usage as `PROVIDES_FIELDS_HAS_ARGUMENTS`.

**Example:**

```graphql
# Source schema (Reviews subgraph)
type Review {
  id: ID!
  body: String!
  author: User @provides(fields: "email")
}

type User @key(fields: "id") {
  id: ID!
  email: String! @external
}
```

```graphql
# Composed schema
type Review {
  id: ID!
  body: String!
  author: User
}

type User {
  id: ID!
  email: String!
}
```

> **In C#:** `#!csharp [Provides("email")]` attribute. See [Field Ownership](./field-ownership-and-sharing.md).

---

## `@external`

Marks a field as owned by another subgraph. The current subgraph references the field for entity identification (via `@key`) or to provide it locally through `@provides`.

```graphql
directive @external on FIELD_DEFINITION
```

Every `@external` field must be referenced by at least one `@provides` directive or used in a `@key`. An unused `@external` field causes a composition error. External fields cannot be combined with `@override` or `@provides` on the same field.

**Example -- external field used with `@provides`:**

```graphql
# Source schema (Reviews subgraph)
type Review {
  id: ID!
  author: User @provides(fields: "email")
}

type User @key(fields: "id") {
  id: ID!
  email: String! @external
}
```

**Example -- external field used as entity key:**

```graphql
# Source schema (Payments subgraph)
type Product @key(fields: "sku") {
  sku: String! @external
  price: Float!
}

type Query {
  productBySku(sku: String!): Product @lookup
}
```

> **In C#:** `[External]` attribute (from `HotChocolate.ApolloFederation.Types`). See [Field Ownership](./field-ownership-and-sharing.md).

---

## `@override`

Migrates field ownership from one subgraph to another. The current subgraph takes responsibility for resolving the field, and the original subgraph stops serving it. The original subgraph does not need to be modified.

```graphql
directive @override(from: String!) on FIELD_DEFINITION
```

| Argument | Type      | Description                                                       |
| -------- | --------- | ----------------------------------------------------------------- |
| `from`   | `String!` | The name of the source schema that originally provided this field |

Use `@override` to move a field to a new subgraph during schema evolution. The overriding subgraph typically marks the entity's key fields as `@external`. Cyclic overrides cause a composition error.

**Example:**

```graphql
# Source schema: original Catalog subgraph (unchanged)
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
}
```

```graphql
# Source schema: new Payments subgraph (takes over price)
type Product @key(fields: "id") {
  id: ID! @external
  price: Float! @override(from: "Catalog")
  tax: Float!
}
```

```graphql
# Composed schema
type Product {
  id: ID!
  name: String!
  price: Float!
  tax: Float!
}
```

> **In C#:** `#!csharp [Override("Catalog")]` attribute (from `HotChocolate.ApolloFederation.Types`). See [Schema Exposure and Evolution](./schema-exposure-and-evolution.md).

---

# Visibility

## `@internal`

Hides a type or field from the composite schema and excludes it from the standard schema-merging process. The gateway can still use internal fields as lookup entry points for entity resolution.

```graphql
directive @internal on OBJECT | FIELD_DEFINITION
```

Internal types and fields do not collide with similarly named fields or types on other source schemas, because they bypass merge rules entirely. Use `@internal` to create resolution-only entry points that clients cannot query directly.

**Example:**

```graphql
# Source schema A
type Query {
  productById(id: ID!): Product @lookup
  productBySku(sku: ID!): Product @lookup @internal
}

type Product @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: ID!
  name: String!
}
```

```graphql
# Composed schema (internal lookup removed)
type Query {
  productById(id: ID!): Product
}

type Product {
  id: ID!
  sku: ID!
  name: String!
}
```

> **In C#:** `[Internal]` attribute. See [Schema Exposure and Evolution](./schema-exposure-and-evolution.md).

---

## `@inaccessible`

Prevents a type system member from appearing in the client-facing composite schema, even if it is accessible in the underlying source schemas.

```graphql
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
```

Unlike `@internal`, inaccessible elements still participate in composition merging and can be referenced by `@require` dependencies in other subgraphs. If any source schema marks a type system member as `@inaccessible`, it is hidden from the composite schema -- even if other schemas expose the same member without `@inaccessible`.

**Example:**

```graphql
# Source schema A
type Product @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: String! @inaccessible
  name: String!
}
```

```graphql
# Source schema B
type Product @key(fields: "sku") {
  sku: String!
  price: Float!
}
```

```graphql
# Composed schema (sku hidden by schema A's @inaccessible)
type Product {
  id: ID!
  name: String!
  price: Float!
}
```

> **In C#:** `[Inaccessible]` attribute. See [Schema Exposure and Evolution](./schema-exposure-and-evolution.md).

---

# `@internal` vs `@inaccessible`

These two directives both hide elements from the composite schema, but they behave differently during composition:

| Aspect         | `@internal`                      | `@inaccessible`                       |
| -------------- | -------------------------------- | ------------------------------------- |
| Scope          | Local to its source schema       | Global across all subgraphs           |
| Merge behavior | Bypasses merge rules entirely    | Participates in merge, then hidden    |
| Collision      | No collisions with other schemas | Hides even if other schemas expose it |
| Use case       | Internal lookup entry points     | Restrict client access to fields      |

Use `@internal` when a field or type exists solely for the gateway's entity resolution and should not interact with other schemas at all. Use `@inaccessible` when a field carries data that other subgraphs may depend on through `@require`, but clients should not query it directly.

---

# Federated Event Streams

## `@eventStream`

Backs a subscription root field with a message broker (NATS, Kafka, Azure Event Hubs, Amazon SQS, or Redis). The gateway subscribes to the configured topics and, for each event, resolves the field's selection set across the subgraphs from the event payload.

```graphql
directive @eventStream(
  message: FieldSelectionSet!
  topics: [String!]
  broker: String
) on FIELD_DEFINITION
```

| Argument  | Type                 | Description                                                                                                                             |
| --------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `message` | `FieldSelectionSet!` | The shape of the event payload, as a selection set over the field's return type (e.g. `"{ id }"`).                                      |
| `topics`  | `[String!]`          | The broker topic(s) to subscribe to. Supports `{$args.<name>}` templates. When omitted, inferred from the field name and its arguments. |
| `broker`  | `String`             | The name of the registered broker. Defaults to the unnamed broker.                                                                      |

**Example -- broker-backed subscription:**

```graphql
# Source schema (Products subgraph)
type Subscription {
  onProductPriceChanged(productId: ID!): Product @eventStream(message: "{ id }")
}
```

```graphql
# Composed schema
type Subscription {
  onProductPriceChanged(productId: ID!): Product
}
```

The directive is removed from the client-facing schema; the gateway records the broker binding on its internal subscribe metadata.

> **In C#:** `[EventStream("...")]` attribute or `.EventStream(...)`. See [Subscriptions](./subscriptions.md).

---

## `@eventCursor`

Enables resumable subscriptions. On a subscription field argument it marks the input that accepts a resume cursor; on an output field it marks the value that carries each event's cursor for the client to store and replay after a reconnect.

```graphql
directive @eventCursor on ARGUMENT_DEFINITION | FIELD_DEFINITION
```

The marked argument and field must both be of type `String`. A subscription field may declare at most one cursor argument and at most one cursor field.

**Example -- resumable subscription:**

```graphql
# Source schema (Products subgraph)
type Subscription {
  onProductPriceChanged(
    productId: ID!
    after: String @eventCursor
  ): ProductPriceChange @eventStream(message: "{ product { id } }")
}

type ProductPriceChange {
  product: Product!
  cursor: String @eventCursor
}
```

```graphql
# Composed schema
type Subscription {
  onProductPriceChanged(productId: ID!, after: String): ProductPriceChange
}

type ProductPriceChange {
  product: Product!
  cursor: String
}
```

During composition the two `@eventCursor` markers are recorded on the gateway's internal subscribe metadata (as `cursorField` and `cursorArgument`), so the gateway knows which field surfaces the cursor and which argument resumes the stream.

> **In C#:** `[EventCursor]` attribute or `.EventCursor()`. See [Client-resumable subscriptions](./subscriptions.md#client-resumable-subscriptions).

---

# See Also

- [Cost Analysis](./cost-analysis.md) -- How source `@cost` and `@listSize` metadata is derived and enforced
- [GraphQL Composite Schemas Specification](https://graphql.github.io/composite-schemas-spec/draft/) -- The specification that defines these directives
- [Getting Started](./getting-started.md) -- Introduction to Fusion in practice
- [Entities and Lookups](./entities-and-lookups.md) -- Entity resolution patterns with `@key`, `@lookup`, and `@is`
- [Field Ownership](./field-ownership-and-sharing.md) -- Ownership model with `@shareable`, `@external`, and `@provides`
- [Data Requirements](./data-requirements-and-mapping.md) -- Cross-subgraph data dependencies with `@require`
- [Schema Exposure and Evolution](./schema-exposure-and-evolution.md) -- Visibility control with `@internal`, `@inaccessible`, and `@override`
- [Subscriptions](./subscriptions.md) -- Federated event streams with `@eventStream` and `@eventCursor`
- [Cache Control](./cache-control.md) -- CDN and HTTP caching behavior
- [Composition](./composition.md) -- How directives affect schema merging

---

### Website Content Docs Fusion Entities And Lookups (`website/content/docs/fusion/entities-and-lookups.md`)

---
title: "Entities and Lookups"
description: "How entities work in Hot Chocolate Fusion: define key-based identity and use @lookup fields so the gateway can resolve types across subgraphs."
---

Entities are the mechanism that makes distributed GraphQL work. They are types with stable keys that can be referenced and resolved across subgraphs. For example, the Products subgraph defines the `Product` type, and the Reviews subgraph contributes the `reviews` field to `Product`. The Accounts subgraph defines the `User` type, and other subgraphs can contribute additional fields to `User`. Without entities, each subgraph would be an isolated API. With entities, those subgraphs compose into one unified API.

This page explains entity resolution in more detail: how entities are defined and how lookups resolve them across subgraphs. If you completed the [Getting Started](./getting-started.md) tutorial, you already used these concepts. Here, you will focus on the mechanics and patterns behind them.

# What Makes a Type an Entity

A type is not an entity because it appears in multiple subgraphs. It is an entity because it has stable key-based identity. An entity is a type with one or more key fields that uniquely identify an instance across multiple subgraphs. Those key fields form the contract between subgraphs: one subgraph can return an entity reference by key, and another subgraph can use that key to resolve additional fields for the same instance.

In practice, two requirements matter:

1. **Entity identity:** one or more key fields uniquely identify each instance, like `id` or `sku`.
2. **Entity resolution:** at least one lookup is available so the gateway can resolve references by key.

```graphql
# Products subgraph
type Product {
  id: ID!
  name: String!
}

type Query {
  productById(id: ID!): Product @lookup
}
```

Another subgraph can reuse the same key and contribute fields.

```graphql
# Reviews subgraph
type Product {
  id: ID!
  reviews: [Review!]!
}

type Query {
  productById(id: ID!): Product @lookup @internal
}
```

In these examples, `id` is the key and `@lookup` defines how `Product` is resolved by that key. The Reviews lookup is internal, so clients cannot call it directly, but the gateway can use it to enter the Reviews subgraph and resolve `reviews`.

# Lookups

A lookup is a query field that resolves an entity by its key. The gateway uses lookups to fetch additional fields for entities. Depending on the requested fields and available routes, it can use any subgraph that provides those fields and a compatible lookup path. Without a lookup, the gateway has no way to enter a subgraph and resolve an entity.

## Public Lookups

A public lookup serves two purposes: clients can call it directly as a query field, and the gateway uses it for entity resolution behind the scenes.

**GraphQL schema**

```graphql
type Query {
  productById(id: ID!): Product @lookup
}
```

The `@lookup` directive marks `productById` as a lookup for `Product`. Because the argument is named `id`, composition maps it to the `Product.id` key field. Cross-subgraph transitions depend on this mapping: the source subgraph must provide the key, and the target subgraph must expose a lookup that accepts that key.

Lookups must return nullable entity types. In GraphQL, that means `Product` instead of `Product!`. In C#, use a nullable return type like `Product?`. This allows unresolved keys to return `null` and helps avoid cascading failures when one or more subgraphs cannot provide additional fields for an entity.

In Hot Chocolate, the `@lookup` directive is represented by the `[Lookup]` attribute.

**C# resolver**

```csharp
[QueryType]
public static partial class ProductQueries
{
    [Lookup]
    public static async Task<Product?> GetProductByIdAsync(
        int id,
        IProductByIdDataLoader productById,
        CancellationToken cancellationToken)
        => await productById.LoadAsync(id, cancellationToken);
}
```

If the argument name does not match a field name on the type, the `@is` directive must be used to map the argument to the key field.

```graphql
type Query {
  product(productId: ID! @is(field: "id")): Product @lookup
}
```

In Hot Chocolate, you can use the `[Is]` attribute with `nameof`.

```csharp
[QueryType]
public static partial class ProductQueries
{
    [Lookup]
    public static async Task<Product?> GetProductAsync(
        [Is(nameof(Product.Id))] int productId,
        IProductByIdDataLoader productById,
        CancellationToken cancellationToken)
        => await productById.LoadAsync(productId, cancellationToken);
}
```

## Internal Lookups

An internal lookup is hidden from the composite schema. Clients cannot call it directly. It exists only for the gateway to use during entity resolution.

**GraphQL schema**

```graphql
type Query {
  productById(id: ID!): Product @internal @lookup
}
```

The `@internal` directive tells the composition to exclude this lookup from the public composite schema. The gateway can still use it when it needs to enter the Reviews subgraph to resolve `Product.reviews`, but clients never see or call it.

**C# resolver**

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

You can also group internal lookups under a dedicated internal root field. This keeps internal routing entry points in one place and avoids repeating `@internal` on every lookup field.

**GraphQL schema (grouped internal lookups)**

```graphql
type Query {
  internalLookups: InternalLookups @internal
}

type InternalLookups @internal {
  productByTenantAndSku(tenantId: ID!, sku: String!): Product @lookup
}
```

**C# declaration**

```csharp
[QueryType]
public static partial class Query
{
    [Internal]
    public static InternalLookups GetInternalLookups { get; } = new();
}

[Internal, ObjectType]
public partial class InternalLookups
{
    [Lookup]
    public Product? GetProductByTenantAndSku(int tenantId, string sku)
        => ProductRepository.GetByTenantAndSku(tenantId, sku);
}
```

In this pattern, clients cannot access `internalLookups` from the composite schema, but the gateway can still use nested `@lookup` fields for internal transitions.

## When to Use Internal vs. Public Lookups



Use a **public lookup** when:

- Your subgraph is the primary owner of the entity. For example, the Products subgraph owns `Product`, and the Accounts subgraph owns `User`.
- Clients should be able to query for this entity directly from your subgraph
- The lookup validates that the entity exists and returns `null` if it does not

Use an **internal lookup** when:

- Your subgraph extends an entity and merely contributes extra fields to it, for example the Reviews subgraph adds the `reviews` field to the `Product` entity.
- You do not want clients to enter your subgraph through this lookup
- The lookup just constructs a stub. It does not validate the existence of the entity.

For cross-subgraph resolution to work, a subgraph that contributes fields to an entity must expose a compatible lookup path for that entity. This can be a direct lookup field or a nested lookup under an internal root object. If no lookup path exists, transitions into that subgraph are unsatisfiable.

## Multiple Lookups Per Entity

An entity can have multiple lookups, even in the same subgraph. This is useful when an entity can be identified by different keys. It is especially helpful when different subgraphs reference the same entity through different keys, for example `User.id` in one place and `User.username` in another. By providing both lookups, the gateway can transition into the target subgraph from either reference shape.

**GraphQL schema**

```graphql
type Query {
  userById(id: ID!): User @lookup
  userByUsername(username: String!): User @lookup
}
```

**C# resolver**

```csharp
[QueryType]
public static partial class UserQueries
{
    [Lookup]
    public static async Task<User?> GetUserById(
        int id,
        IUserByIdDataLoader userById,
        CancellationToken cancellationToken)
        => await userById.LoadAsync(id, cancellationToken);

    [Lookup]
    public static async Task<User?> GetUserByUsername(
        string username,
        IUserByNameDataLoader userByName,
        CancellationToken cancellationToken)
        => await userByName.LoadAsync(username, cancellationToken);
}
```

The Accounts subgraph defines two lookups for `User`: one by `id` and one by `username`. The gateway can resolve a User reference using whichever key is available. If another subgraph references a User by username, the gateway uses `GetUserByUsername`.

With more modern GraphQL servers you can also use the finder pattern with the `@oneOf` directive.

```graphql
type Query {
  user(by: UserByInput! @is(field: "{ id } | { username }")): User @lookup
}

input UserByInput @oneOf {
  id: ID
  username: String
}
```

In this case we use the `@is` directive with the choice operator `|` to signal to Fusion that it can use this lookup either with the `id` or the `username` as a key.

## Composite Keys

Some entities are identified by a combination of fields instead of a single field. In that case, the lookup arguments together form the key.

**GraphQL schema**

```graphql
# Inventory subgraph
type Product {
  tenantId: ID!
  sku: String!
  inStock: Boolean!
}

type Query {
  productByTenantAndSku(tenantId: ID!, sku: String!): Product @lookup
}
```

**C# resolver**

```csharp
[QueryType]
public static partial class ProductQueries
{
    [Lookup]
    public static Product? GetProductByTenantAndSku(
        int tenantId,
        string sku)
        => ProductRepository.GetByTenantAndSku(tenantId, sku);
}
```

Here, `tenantId` and `sku` are both required to identify `Product`. During planning, Fusion can transition to this lookup only when both key values are available.

If the lookup arguments do not match entity field names directly, you can map them with the `@is` directive and even pull up fields.

**GraphQL schema with per-argument mapping**

```graphql
type Product {
  sku: String!
  inStock: Boolean!
  tenant: Tenant
}

type Tenant {
  id: ID!
}

type Query {
  product(
    tenantId: ID! @is(field: "tenant.id")
    sku: String! @is(field: "sku")
  ): Product @lookup
}
```

**GraphQL schema with input-object mapping**

```graphql
type Product {
  sku: String!
  inStock: Boolean!
  tenant: Tenant
}

type Tenant {
  id: ID!
}

input ProductKeyInput {
  tenantId: ID!
  sku: String!
}

type Query {
  product(
    key: ProductKeyInput! @is(field: "{ tenantId: tenant.id, sku }")
  ): Product @lookup
}
```

Both variants describe the same composite key. The first maps each argument explicitly. The second maps the input object fields in one selection map.

> The FieldSelectionMap syntax from the Composite Schemas specification supports more advanced argument-to-field mappings for lookups. For the full grammar and examples, see the [Composite Schemas specification](https://graphql.github.io/composite-schemas-spec/draft/#sec-Appendix-A-Specification-of-FieldSelectionMap-Scalar).

# Explicit Key Declaration

In most cases, you do not need to declare entity keys explicitly. The composition engine infers keys from your lookup fields.

Sometimes you need or want to declare the key explicitly. Use the `@key` directive when:

- You want to be explicit about which fields form the key on the entity itself
- You want to declare key identity on a type independently of local lookup inference

**GraphQL schema**

```graphql
type Product @key(fields: "id") {
  id: ID!
}
```

**C# type declaration**

```csharp
[EntityKey("id")]
public sealed record Product([property: ID<Product>] int Id);
```

The `#!sdl @key(fields: "id")` directive explicitly declares that `Product` is identified by the `id` field. This is useful when key identity should be declared explicitly instead of inferred from lookup arguments.

`@key` declares identity. It does not replace lookup paths. If a subgraph contributes fields and needs to be entered during planning, it still needs a compatible lookup route.

The `fields` value uses GraphQL field names, not C# member names.

Key fields may supply constant arguments to select a specific variant of a field. For example, `#!sdl @key(fields: "id(scope: LOCAL)")` selects the `id` field with the constant argument `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.

An entity can have multiple keys. Each `@key` directive on a type represents one key.

**GraphQL schema with scalar composite key**

```graphql
type Product @key(fields: "id") @key(fields: "sku category") {
  id: ID!
  sku: String
  category: String
}
```

**GraphQL schema with nested composite key**

```graphql
type Product @key(fields: "id") @key(fields: "sku tenant { id }") {
  id: ID!
  sku: String
  tenant: Tenant
}

type Tenant {
  id: ID!
}
```

# GraphQL Global Object Identification

If your subgraphs implement GraphQL Global Object Identification, with a `node` field on `Query` and a `Node` interface, you already have a strong entity identity contract. You can build on this by using `node` as a lookup and treating types that implement `Node` as entities.

**GraphQL schema**

```graphql
type Query {
  node(id: ID!): Node @lookup
}

interface Node @key(fields: "id") {
  id: ID!
}
```

If you are using Hot Chocolate as a subgraph, set `MarkNodeFieldAsLookup` and Hot Chocolate will mark the generated `node` field as a lookup automatically.

**C# configuration**

```csharp
builder
    .AddGraphQL()
    .AddGlobalObjectIdentification(o => o.MarkNodeFieldAsLookup = true);
```

Set `ApplyInaccessibleToNodeFields` if you want the generated `node` and `nodes` fields to remain in your source schema while being hidden from the client-facing composite schema. They are marked `@inaccessible` and `@shareable`, since several source schemas can contribute them, and `node` keeps working as a lookup for entity resolution. This hides them under standard composition settings, while a composition with global object identification enabled keeps the gateway's own `node` field.

**C# configuration**

```csharp
builder
    .AddGraphQL()
    .AddGlobalObjectIdentification(o => o.MarkNodeFieldAsLookup = true)
    .ModifyOptions(o => o.ApplyInaccessibleToNodeFields = true);
```

> If GraphQL Global Object Identification is enabled at the gateway level, every entity resolvable through the `node` field becomes a public entry point. Use explicit internal lookups for entities you do not want exposed as public entry points.

# Next Steps

- **Need to add fields to an interface from another source?** See [Interface Objects](./interface-objects.md).
- **Need field ownership contracts?** See [Field Ownership](./field-ownership-and-sharing.md).
- **Need argument mapping and cross-subgraph dependencies?** See [Data Requirements](./data-requirements-and-mapping.md) for `@is`, `@require`, and FieldSelectionMap patterns.
- **Need runtime performance guidance?** See Hot Chocolate docs for DataLoader and batching patterns used inside lookup resolvers.
- **Ready to go to production?** See [Authentication and Authorization](./authentication-and-authorization.md) for securing your gateway and subgraphs, or [Deployment and CI/CD](./deployment-and-ci-cd.md) for setting up independent subgraph deployments.

---

### Website Content Docs Fusion Field Ownership And Sharing (`website/content/docs/fusion/field-ownership-and-sharing.md`)

---
title: "Field Ownership"
description: "Fusion field ownership explained: which subgraph owns each field, when to use @shareable for intentional overlap, and how @external and @provides fit in."
---

Field ownership defines which subgraph is responsible for each field in the composite schema. Clear ownership boundaries keep composition predictable and prevent semantic drift across teams.

This chapter explains Fusion's ownership model, when to use `@shareable`, and how `@external` and `@provides` fit into ownership contracts.

# Default Ownership Rules

For non-key fields, Fusion expects a single owner.

- If one subgraph defines `Product.price`, that subgraph owns `Product.price`.
- If multiple subgraphs define the same non-key field without an explicit sharing contract, composition fails.

Key fields are special. Fields used for entity identity and lookup mapping can appear in multiple subgraphs as part of entity resolution.

# Shared Fields

Use `@shareable` when the same field is intentionally defined in multiple subgraphs and has the same meaning and value semantics in each definition.

`@shareable` is a contract, not just a conflict suppressor.

- It signals intentional overlap.
- It requires team alignment on field semantics.
- It allows the gateway to resolve the field from different subgraphs depending on the operation plan.

Let's assume we have two subgraphs, both define `User.name` as `@shareable`, so composition succeeds and the gateway can resolve the field from either source:

**GraphQL schema**

```graphql
# Accounts subgraph
type User {
  id: ID!
  name: String! @shareable
}

# Reviews subgraph
type User {
  id: ID!
  name: String! @shareable
  reviews: [Review!]!
}
```

**C# declaration**

```csharp
[ObjectType<User>]
public static partial class UserNode
{
    [Shareable]
    public static string GetName([Parent] User user)
        => user.Name!;
}
```

## When Not to Share

Do not use `@shareable` when fields are only superficially similar.

- Different semantics: `displayName` vs internal account name.
- Different staleness guarantees.
- Different normalization rules.

If the meaning differs, use different field names and keep a single owner per field.

# Contextual Field Availability

`@provides` declares that a field returning an entity can also provide selected subfields of that entity in that specific path. This is a contextual optimization, not a transfer of global ownership.

`@external` marks these field as owned by another subgraph.

**GraphQL schema**

```graphql
# Reviews subgraph
type Review {
  id: ID!
  author: User @provides(fields: "username")
}

type User {
  id: ID!
  username: String! @external
}
```

In this example:

- Accounts still owns `User.username`.
- Reviews can provide `username` only when resolving `Review.author`.

For detailed `@provides` patterns and FieldSelectionMap syntax, see [Data Requirements and Mapping](./data-requirements-and-mapping.md).

# Common Ownership Failures

These are the most frequent ownership mistakes that cause composition to fail or produce unexpected behavior.

## Duplicate Non-Key Field Without Sharing

If two subgraphs define the same non-key field without `@shareable`, composition fails.

Typical fix:

1. Remove the duplicate field from one subgraph, or
2. Mark all definitions with `@shareable` and align semantics.

## Misusing Provides as Ownership

`@provides` is a load optimization for partial availability. Use it when only some paths in a subgraph can supply a field. If your subgraph can always provide a field, use `@shareable` instead. Marking it `@external` and adding `@provides` to every path that returns the entity adds complexity for no benefit.

# Ownership Checklist

Before composition, verify:

1. Every non-key field has one clear owner.
2. Every shared field is explicitly marked `@shareable` in all defining subgraphs.
3. Shared fields have aligned semantics across teams.
4. `@external` is used only for fields owned elsewhere.
5. `@provides` is used for contextual availability, not to hide ownership ambiguity.

# Next Steps

- **Need identity and lookup routing?** See [Entities and Lookups](./entities-and-lookups.md).
- **Need to add a default field to every implementation of an interface?** See [Interface Objects](./interface-objects.md).
- **Need dependency and mapping syntax?** See [Data Requirements and Mapping](./data-requirements-and-mapping.md).
- **Need merge and validation rules?** See [Composition](./composition.md).

---

### Website Content Docs Fusion Interface Objects (`website/content/docs/fusion/interface-objects.md`)

---
title: "Interface Objects"
description: "Extend an interface from another Fusion source schema with @interfaceObject, project fields to its implementations, and recover concrete identity through lookups."
---

An interface object lets one source schema add fields to an interface without declaring every type that implements it. This is useful when a source owns behavior that applies to all implementations, but another source owns the interface and its concrete types.

This page explains how Fusion composes a same-named object stand-in, projects its fields, and uses lookups to recover concrete types. The examples focus on the source-schema contracts and the resulting query flow.

Before you begin, make sure you understand [entity keys and lookups](./entities-and-lookups.md). Interface objects use the same identity and lookup contracts to move values between source schemas.

# Extend an Interface from Another Source

The Catalog source owns a `Media` interface and knows its concrete `Book` and `Movie` types. It also exposes a lookup that returns the interface.

**Catalog source schema**

```graphql
type Query {
  mediaById(id: ID!): Media @lookup
}

interface Media {
  id: ID!
  title: String!
}

type Book implements Media @key(fields: "id") {
  id: ID!
  title: String!
  isbn: String!
}

type Movie implements Media @key(fields: "id") {
  id: ID!
  title: String!
  runtime: Int!
}
```

The Analytics source does not need to declare `Book` or `Movie`. Instead, it declares an object named `Media` and marks it with `@interfaceObject`.

**Analytics source schema**

```graphql
type Query {
  trendingMedia: [Media!]!
  mediaByKey(id: ID!): Media @lookup @internal
}

type Media @interfaceObject @key(fields: "id") {
  id: ID!
  views: Int!
}
```

The object name must match the interface name. Fusion infers `id` as a `Media` key from the Catalog lookup. The stand-in must declare the matching key explicitly. Fusion uses this key to correlate an opaque `Media` value with a concrete value from another source.

The `views` field becomes part of `Media` and is projected as a default field onto every compatible implementation. The client-facing schema contains this shape:

```graphql
interface Media {
  id: ID!
  title: String!
  views: Int!
}

type Book implements Media {
  id: ID!
  title: String!
  isbn: String!
  views: Int!
}

type Movie implements Media {
  id: ID!
  title: String!
  runtime: Int!
  views: Int!
}
```

The internal `mediaByKey` lookup gives Fusion a route back into Analytics when it needs a projected field. `@internal` hides this routing field from clients. See [Internal Lookups](./entities-and-lookups.md#internal-lookups) for the complete pattern.

> A stand-in that contributes a non-key field must have a lookup in its own source schema. Declaring a matching type and key without a lookup is not enough to make the projected field reachable.

# Query Fields from the Stand-In

When a query selects only fields that Analytics can provide, Fusion can resolve the operation from that source.

```graphql
query TrendingMedia {
  trendingMedia {
    id
    views
  }
}
```

For one media item, the result can look like this:

```json
{
  "data": {
    "trendingMedia": [
      {
        "id": "TWVkaWE6MQ==",
        "views": 123
      }
    ]
  }
}
```

Fusion performs one Analytics fetch. It does not ask Catalog for the concrete type because the operation does not observe concrete identity.

# Recover Concrete Types

Analytics knows that a value has the `Media` shape, but it does not know whether that value is a `Book`, `Movie`, or another implementation. Values returned through the stand-in are therefore opaque.

Selecting `__typename` or a concrete inline fragment makes the concrete identity observable. Fusion then uses the key from Analytics to call the covering interface lookup in Catalog.

```mermaid
flowchart LR
    C[Client] --> G[Fusion gateway]
    G -->|trendingMedia| A[Analytics stand-in]
    A -->|opaque Media and id| G
    G -->|mediaById using id| K[Catalog]
    K -->|Book or Movie| G
    G --> C
```

The Catalog lookup is a covering lookup because it returns `Media` and its source can resolve every possible concrete type in the composite schema.

```graphql
query TrendingMedia {
  trendingMedia {
    __typename
    id
    views
    ... on Book {
      isbn
    }
    ... on Movie {
      runtime
    }
  }
}
```

For a `Book`, the result can look like this:

```json
{
  "data": {
    "trendingMedia": [
      {
        "__typename": "Book",
        "id": "TWVkaWE6MQ==",
        "views": 123,
        "isbn": "Book: TWVkaWE6MQ=="
      }
    ]
  }
}
```

The data flow has four steps:

1. Analytics returns the `id` and `views` fields for each opaque `Media` value.
2. Fusion batches Catalog `mediaById` lookups for the returned IDs.
3. Catalog returns the authoritative `__typename` and concrete fields.
4. Fusion merges the concrete data into the original result.

A covering lookup must meet all of these conditions:

- It returns the interface, not one concrete implementation.
- Its arguments can be populated from the stand-in key.
- One source schema containing the lookup covers every possible implementation in the composite schema.

Several concrete lookups spread across different source schemas do not form one covering lookup.

## Resolve a Stand-In Field Inside a Concrete Fragment

When you select a projected field inside a concrete fragment, Analytics cannot return that field until Fusion knows whether the fragment applies. This produces an Analytics to Catalog to Analytics route (B to A to B):

```graphql
query TrendingBooks {
  trendingMedia {
    ... on Book {
      views
    }
  }
}
```

For the earlier `Book` value, the result can look like this:

```json
{
  "data": {
    "trendingMedia": [
      {
        "views": 123
      }
    ]
  }
}
```

Fusion resolves this query in three source operations:

1. Analytics returns each opaque value's `id`.
2. Catalog uses `mediaById` to recover `Book` as the concrete type.
3. Analytics uses `mediaByKey` to fetch `views` for values that match the `Book` fragment.

Fusion carries the `id` from the first operation through both lookups.

# Replace a Projected Default

An implementation normally adopts a field projected from the interface object. To provide a different implementation for one concrete type, declare the field on that type and mark it with `@implement`.

```graphql
type Book implements Media @key(fields: "id") {
  id: ID!
  title: String!
  isbn: String!
  views: Int! @implement
}
```

This declaration tells composition that Catalog intentionally replaces the Analytics default for `Book.views`. Other implementations, such as `Movie`, continue to use the projected default.

If you declare `Book.views` without `@implement`, composition reports `INTERFACE_OBJECT_FIELD_REQUIRES_IMPLEMENT`. If there is no projected default to replace, `@implement` reports `IMPLEMENT_WITHOUT_DEFAULT`.

# Share Defaults from Unrelated Interfaces

An object can implement unrelated interfaces whose stand-ins project a field with the same name. When the field has the same contract and result in every source, mark every contributing declaration with `@shareable`.

The following three source schemas define the interfaces, a concrete type that implements both, and the lookups needed to resolve the projected field.

**Catalog source schema**

```graphql
type Query {
  physicalProductById(id: ID!): PhysicalProduct @lookup
  digitalProductById(id: ID!): DigitalProduct @lookup
}

interface PhysicalProduct @key(fields: "id") {
  id: ID!
}

interface DigitalProduct @key(fields: "id") {
  id: ID!
}

type Chair implements PhysicalProduct & DigitalProduct @key(fields: "id") {
  id: ID!
}
```

**Physical reviews source schema**

```graphql
type Query {
  physicalProductByKey(id: ID!): PhysicalProduct @lookup @internal
}

type PhysicalProduct @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]! @shareable
}

type Review {
  rating: Int! @shareable
}
```

**Digital reviews source schema**

```graphql
type Query {
  digitalProductByKey(id: ID!): DigitalProduct @lookup @internal
}

type DigitalProduct @interfaceObject @key(fields: "id") {
  id: ID!
  reviews: [Review!]! @shareable
}

type Review {
  rating: Int! @shareable
}
```

Both defaults project `reviews` onto `Chair`. If any contributing declaration is not shareable, composition reports `INVALID_PROJECTED_FIELD_SHARING`. Use `@shareable` only when the field semantics match. See [Field Ownership](./field-ownership-and-sharing.md) for the full sharing contract.

# Use Interface Objects with Apollo Federation

Apollo Federation v2 subgraphs use the same `#!sdl type Media @interfaceObject @key(...)` stand-in shape, imported through `@link`. Fusion composes that construct with native interface objects.

Resolution still follows each source protocol. Fusion enters native GraphQL Federation sources through `@lookup` fields and Apollo Federation sources through `_entities` representations. Apollo Federation has no `@implement` directive. When an Apollo implementation redeclares a projected default, mark the compatible declarations with `@shareable`; Fusion uses that declaration as the explicit replacement contract.

For Apollo-specific translation, runtime behavior, and non-resolvable key compatibility, see the [Apollo Federation Connector](./connectors/apollofederation.md#interface-objects).

# Troubleshooting

## `INTERFACE_OBJECT_KEY_MISSING`

The stand-in has no key. Add at least one key that identifies the interface value and can be passed to its lookups.

```graphql
type Media @interfaceObject @key(fields: "id") {
  id: ID!
}
```

## `INTERFACE_OBJECT_NO_INTERFACE`

No source schema defines an interface with the stand-in's name. Define the real interface and its implementing types in at least one concrete-aware source.

## `INTERFACE_OBJECT_KEY_MISMATCH`

The stand-in key does not match a key on the real interface. Use one of the interface entity's keys on the stand-in.

## Source schema 'B' contributes fields to 'Media' but provides no lookup to resolve them

This `UNSATISFIABLE_QUERY_PATH` message means a stand-in adds a non-key field, but its source has no lookup returning that stand-in. Add a nullable lookup. You can mark it `@internal` when clients should not call it.

```graphql
type Query {
  mediaByKey(id: ID!): Media @lookup @internal
}
```

## `UNSATISFIABLE_QUERY_PATH`: no lookup covers the possible types

The diagnostic identifies the uncovered types and the source schemas that introduced them. For example:

> The query path 'Query.topReviewed' cannot be satisfied: values of 'Media' produced by source schema 'B' are opaque, and no source schema provides a lookup for 'Media' that covers the possible type(s) 'Photo' introduced by source schema(s) 'C'.

An opaque value can reach a client, but no concrete-aware source can recover every possible runtime type. Add an interface lookup to one source schema and make every composite implementation resolvable there.

See [Diagnosing `UNSATISFIABLE_QUERY_PATH` Diagnostics](./composition.md#diagnosing-unsatisfiable_query_path-diagnostics) for help reading the complete lookup failure tree.

# Next Steps

- Define stable identity and routing paths: [Entities and Lookups](./entities-and-lookups.md).
- Review sharing and ownership contracts: [Field Ownership](./field-ownership-and-sharing.md).
- Look up exact directive signatures: [Directive Reference](./directives-reference.md#interface-objects).
- Connect Apollo Federation subgraphs: [Apollo Federation Connector](./connectors/apollofederation.md#interface-objects).

---

### Website Content Docs Fusion Local Development (`website/content/docs/fusion/local-development.md`)

---
title: "Local GraphQL Federation Development with Aspire"
description: "Develop GraphQL subgraphs locally in isolation, then compose them into your full graph and test changes with Aspire and ChilliCream's Fusion gateway."
---

Fusion's local development experience is built on [Aspire](https://aspire.dev): an AppHost, written in C# or TypeScript, starts your subgraphs and composes them into a running gateway. Without it, every change to a type, a field, or a resolver means re-exporting the schema, re-composing with the Nitro CLI, and restarting the gateway.

The `HotChocolate.Fusion.Aspire` package integrates composition into the Aspire AppHost. When you run the AppHost, the orchestrator starts your subgraphs, fetches their source schemas from live endpoints, composes them into a Fusion archive, and writes it to the gateway project directory. This startup flow replaces the manual export-compose-restart cycle. Mark each local subgraph with `WithGraphQLHttpEndpoint()` so composition uses the schema exposed by the running service. The examples on this page show both AppHost languages.

If different teams own subgraphs in separate repositories, bind each team's AppHost to a shared Nitro stage. The AppHost composes the live schemas from the team's local subgraphs with the stage's published fusion configuration. That configuration supplies the schemas and routes for the remaining subgraphs. You can then develop and debug your team's subgraphs inside the full graph without copying another team's schema artifacts into your repository. See [Developing Across Teams and Repositories](#developing-across-teams-and-repositories).

# Prerequisites

You need an Aspire AppHost project with the `HotChocolate.Fusion.Aspire` package.

<LanguageTabs>
<CSharp>

If you do not have an AppHost yet, create it with:

```shell
dotnet new aspire-apphost -n AppHost
```

Add the Fusion Aspire package to the AppHost project:

```shell
cd AppHost
dotnet add package HotChocolate.Fusion.Aspire
```

</CSharp>
<TypeScript>

If you do not have an AppHost yet, create it with the Aspire CLI:

```shell
aspire new aspire-ts-empty -n AppHost -o .
```

`aspire add` does not list `HotChocolate.Fusion.Aspire`, so add the package to the `packages` section of `aspire.config.json` by hand:

```json filename="aspire.config.json"
{
  "packages": {
    "HotChocolate.Fusion.Aspire": "<version>"
  }
}
```

Then restore the AppHost, which regenerates the typed SDK under `.aspire/modules`:

```shell
aspire restore
```

</TypeScript>
</LanguageTabs>

Each subgraph project needs a `schema-settings.json` file in its project directory so the orchestrator can identify the source schema and choose how to fetch it. Generate this file with the `schema export` command from `HotChocolate.AspNetCore.CommandLine`. If you followed [Getting Started](./getting-started.md#export-the-schema), you already have it.

# Setting Up the AppHost

The AppHost wires together your subgraphs and gateway. Three extension methods configure the composition pipeline.

<LanguageTabs>
<CSharp>

```csharp filename="AppHost/Program.cs"
var builder = DistributedApplication.CreateBuilder(args);

builder.AddNitroComposition();

var productsApi = builder
    .AddProject<Projects.Products>("products-api")
    .WithGraphQLHttpEndpoint();

var reviewsApi = builder
    .AddProject<Projects.Reviews>("reviews-api")
    .WithGraphQLHttpEndpoint();

builder
    .AddProject<Projects.Gateway>("gateway-api")
    .WithNitroComposition()
    .WithReference(productsApi)
    .WithReference(reviewsApi);

builder.Build().Run();
```

</CSharp>
<TypeScript>

```typescript filename="apphost.mts"
import { createBuilder } from "./.aspire/modules/aspire.mjs";

const builder = await createBuilder();

await builder.addNitroComposition();

const productsApi = await builder
  .addProject("products-api", "../Products/Products.csproj")
  .withGraphQLHttpEndpoint();

const reviewsApi = await builder
  .addProject("reviews-api", "../Reviews/Reviews.csproj")
  .withGraphQLHttpEndpoint();

await builder
  .addProject("gateway-api", "../Gateway/Gateway.csproj")
  .withNitroComposition()
  .withReference(productsApi)
  .withReference(reviewsApi);

await builder.build().run();
```

</TypeScript>
</LanguageTabs>

Four things to notice:

- **`AddNitroComposition()`** registers the composition orchestrator with the Aspire eventing system. Call this once on the application builder.
- **`WithGraphQLHttpEndpoint()`** declares the GraphQL route of a subgraph and the path its source schema is downloaded from. The orchestrator waits for the subgraph to start, then fetches the source schema over HTTP.
- **`WithNitroComposition()`** marks the gateway as needing composition. The orchestrator discovers all referenced subgraphs, extracts their schemas, composes them, and writes a `gateway.far` file to the gateway project directory.
- **`WithReference()`** is standard Aspire. It tells the orchestrator which subgraphs to include in composition for this gateway.

In a TypeScript AppHost, the same extension methods surface camelCased, optional parameters are gathered into a single options object, and every chained call is awaited.

When you build and run the AppHost, the orchestrator handles the entire composition pipeline automatically. No manual `nitro fusion compose` step needed.

# Live Schema Extraction

`WithGraphQLHttpEndpoint()` declares two paths: `path` is the GraphQL route the subgraph serves (`/graphql` by default), and `schemaPath` is where the schema document is downloaded from (`/graphql/schema.graphql` by default). An Apollo Federation subgraph serves its schema through the GraphQL endpoint at `path` via `_service.sdl`, so `schemaPath` is ignored for it. The orchestrator reads `schema-settings.json` to tell the two kinds of subgraphs apart.

The orchestrator starts each subgraph, waits for it to become healthy, then fetches its schema. If the subgraph is not ready within the timeout, the orchestrator reports an error and the dependent gateway fails to start. Other resources continue running.

You can customize the GraphQL route, the schema download path, and the endpoint name. You can also provide the expected source schema name to validate the fetched schema:

<LanguageTabs>
<CSharp>

```csharp
var productsApi = builder
    .AddProject<Projects.Products>("products-api")
    .WithGraphQLHttpEndpoint(
        path: "/graphql",
        schemaPath: "/graphql/schema.graphql",
        endpointName: "http",
        sourceSchemaName: "Products");
```

</CSharp>
<TypeScript>

```typescript
const productsApi = await builder
  .addProject("products-api", "../Products/Products.csproj")
  .withGraphQLHttpEndpoint({
    path: "/graphql",
    schemaPath: "/graphql/schema.graphql",
    endpointName: "http",
    sourceSchemaName: "Products",
  });
```

</TypeScript>
</LanguageTabs>

Every parameter has a default, so pass only what differs. The source schema name comes from `schema-settings.json`. When you pass `sourceSchemaName`, it acts as an assertion and must match that configured name exactly. It does not rename the source schema, so you can usually omit it.

# Developing Across Teams and Repositories

In an organization where teams own subgraphs in separate repositories, each team can keep an AppHost alongside the subgraphs it develops. Register each local subgraph with `WithGraphQLHttpEndpoint()` so the AppHost fetches its live schema. Bind the AppHost to the graph's shared Nitro stage to compose those local schemas with the published fusion configuration. Nitro supplies the remaining source schemas and routes, so the gateway still represents the full graph while only your team's subgraphs run on your machine.

Sign in once with the Nitro CLI ([installation](./cli.md#installation)). The AppHost reads the session that the CLI stores and never signs in on its own:

```shell
nitro login
```

Then bind the AppHost to a stage and tell the gateway which Nitro API carries its fusion configuration:

<LanguageTabs>
<CSharp>

```csharp filename="AppHost/Program.cs"
var builder = DistributedApplication.CreateBuilder(args);

builder.AddNitroComposition("dev");

var productsApi = builder
    .AddProject<Projects.Products>("products-api")
    .WithGraphQLHttpEndpoint();

builder
    .AddProject<Projects.Gateway>("gateway-api")
    .WithNitroApiId("QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==")
    .WithNitroComposition()
    .WithReference(productsApi);

builder.Build().Run();
```

</CSharp>
<TypeScript>

```typescript filename="apphost.mts"
import { createBuilder } from "./.aspire/modules/aspire.mjs";

const builder = await createBuilder();

await builder.addNitroComposition({ stage: "dev" });

const productsApi = await builder
  .addProject("products-api", "../Products/Products.csproj")
  .withGraphQLHttpEndpoint();

await builder
  .addProject("gateway-api", "../Gateway/Gateway.csproj")
  .withNitroApiId("QXBpCmcwMTk5MGUzNDVlMWU3MjMyYjc2MjYxYzFiNjRkMGQzYg==")
  .withNitroComposition()
  .withReference(productsApi);

await builder.build().run();
```

</TypeScript>
</LanguageTabs>

- **`AddNitroComposition()`** registers the orchestrator for composing only the subgraphs that run locally. **`AddNitroComposition(stage)`** additionally binds the distributed application to one stage in Nitro. Calling both is safe and registers the orchestrator once. Calling `AddNitroComposition(stage)` twice with different stage names throws during AppHost configuration, because a run composes against a single stage.
- **`WithNitroApiId(apiId)`** selects the Nitro API whose fusion configuration a gateway composes against. The API id is the id that the Nitro dashboard and the Nitro CLI report for the API, the same value that `--api-id` takes. Calling the method again replaces the previously configured id. On a resource that is not composed, the API id is accepted as identity metadata for your own tooling and the pull and compose flow ignores it. If you set an API id without calling `AddNitroComposition`, the resource console tells you that it cannot take effect.

## What Happens When a Gateway Starts

Before the orchestrator starts a gateway process that carries an API id:

1. The fusion configuration for that API id and the `AddNitroComposition` stage is downloaded from Nitro. The download runs while the orchestrator waits for the local subgraphs to become healthy, so the two waits do not add up.
2. Each source schema of the distributed application replaces the source schema of the same name in the downloaded configuration. A source schema whose name does not appear there is added to the composition.
3. Each source schema that the distributed application runs is reached at the allocated HTTP endpoint of its Aspire resource. The orchestrator combines that endpoint with the path of the `url` in the subgraph's `schema-settings.json`, or with `/graphql` when the settings define no usable path.
4. Each source schema that only the downloaded configuration carries is external. The gateway reaches it at its `devUrl`, or at its `url` when no `devUrl` is defined. Composition logs a warning for every external source schema without a `devUrl`, because a deployed URL is often not reachable from a developer machine. A subgraph that runs in the local AppHost but has no allocated HTTP endpoint at composition time cannot receive an injected URL either, so it is treated like an external schema for URL resolution, which is why such a resource can also trigger the missing-devUrl warning. See [`transports.http.devUrl`](./cli.md#transports-http-devurl).
5. The composed archive is written to the gateway project directory as usual. The gateway console then reports which fusion configuration it composed against, when that configuration was downloaded, and which external source schemas it carries together with the URLs they resolved to.

The downloaded configuration is the only base the composition builds on. What a previous composition wrote to `gateway.far` is never an input again, so a source schema that was removed or renamed upstream also disappears from your next run.

Variable substitution follows the same split as the URL resolution. The settings of the subgraphs you run resolve against the `Aspire` environment in `schema-settings.json`, while the settings that the downloaded configuration carries resolve against the stage name you passed to `AddNitroComposition`.

A composition or download failure fails only the gateway it belongs to. The rest of the distributed application keeps running.

## Matching Source Schemas by Name

Replacement matches source schemas by name. `WithGraphQLHttpEndpoint()` uses the `name` from the subgraph's `schema-settings.json`. If you pass `sourceSchemaName`, it must match that name exactly or composition fails. It does not rename the source schema.

A local source schema whose configured name does not match the one in Nitro is added next to it instead of replacing it, which usually surfaces as a composition error about a conflicting field. Make sure the `name` in `schema-settings.json` matches the published source schema name.

## Rebuilding a Subgraph

A rebuild or restart of a local subgraph recomposes the gateway schema exactly as it does without Nitro, and the recomposition reuses the fusion configuration that was downloaded when the gateway started. A run therefore downloads once per gateway, and your inner loop stays as fast as a local-only composition. To pick up what was published to the stage in the meantime, use the gateway's Recompose command described below, or restart the AppHost.

When a gateway never acquired a configuration because it failed to start, later composition attempts are skipped with a log entry instead of composing against a stale base.

You do not need to stop the run to restart one subgraph. While `aspire run` keeps the AppHost in the foreground, restart the subgraph from a second terminal with the Aspire CLI:

```shell
aspire resource products-api restart
```

The command executes against the running AppHost. The orchestrator sees the restart, recomposes the schema of every gateway that references the subgraph, and logs the recomposition on the gateway console. When a recomposition fails, the gateway keeps the previous schema. The dashboard offers the same action as Restart in the resource submenu on the Resources page.

The gateway additionally exposes a Recompose command. It downloads a fresh fusion configuration first, so it also picks up what was published to the stage since the run started, without an AppHost restart. Invoke it from the resource submenu of the gateway or from the CLI:

```shell
aspire resource gateway-api recompose
```

## Working Offline

Every downloaded fusion configuration is cached per Nitro API URL, API id, and stage, next to the Nitro CLI configuration (`~/.config/nitro/cache/fusion` on macOS and Linux, `%APPDATA%\nitro\cache\fusion` on Windows). The cache lives outside your repository, so it survives a clean and is shared across working trees.

When the download fails for any reason, including no network, a rejected or expired sign-in, and a stage without a fusion configuration, the gateway composes against the cached copy. Its console gets a warning that the configuration could not be refreshed, names the timestamp of the cached copy, and tells you to run `nitro login` when the sign-in expired. Only when there is no cached copy at all does the gateway fail to start with that reason as its error.

## Continuous Integration and Self-Hosted Nitro

Two environment variables configure the AppHost's connection to Nitro. They carry the same names and the same meaning as in the Nitro CLI, so a shell that is set up for the CLI also configures the AppHost.

| Variable          | Purpose                                                                                                                                                                       |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NITRO_API_KEY`   | Authenticates with an API key instead of an interactive session. It takes precedence over the CLI session file. Set it where `nitro login` cannot run, for example in CI.     |
| `NITRO_CLOUD_URL` | Points the AppHost at a self-hosted Nitro instance. Without it, the AppHost uses the API URL that `nitro login` stored, and `https://api.chillicream.com` when there is none. |

# Composition Settings

Composition settings are configured in Nitro. Open the gateway document and go to Settings > Schema Registry > Composition to control how source schemas are merged when composing the gateway's configuration.



The page carries toggles for global object identification and for removing unreferenced definitions, merge behavior selections for the `@cacheControl` and `@tag` directives, and a tag list that excludes tagged definitions from the composite schema.

Settings apply at publish time. The banner on the page states that changes take effect from the next publish onward, and only when publishing with the `nitro-fusion-publish` GitHub Action, the `NitroFusionPublish` Azure DevOps task, or the `nitro fusion publish` CLI command. A stage keeps the fusion configuration it already carries until that next publish, so changing a setting does not alter what a bound AppHost downloads in the meantime.

The output file name defaults to `gateway.far`. You can change it if needed:

<LanguageTabs>
<CSharp>

```csharp
builder
    .AddProject<Projects.Gateway>("gateway-api")
    .WithNitroComposition(outputFileName: "composed.far")
    .WithReference(productsApi)
    .WithReference(reviewsApi);
```

</CSharp>
<TypeScript>

```typescript
await builder
  .addProject("gateway-api", "../Gateway/Gateway.csproj")
  .withNitroComposition({ outputFileName: "composed.far" })
  .withReference(productsApi)
  .withReference(reviewsApi);
```

</TypeScript>
</LanguageTabs>

# How Composition Fits the Dev Loop

With Aspire, your inner dev loop looks like this:

1. Change code in a subgraph (add a field, modify a type, adjust a resolver).
2. Build and run the AppHost.
3. The orchestrator starts your subgraphs, extracts their schemas, and composes the Fusion archive.
4. The gateway loads the new archive and exposes the updated composite schema.
5. Open Nitro at the gateway endpoint and query immediately.

With `AddNitroComposition`, step 3 composes on top of the fusion configuration that was downloaded when the gateway started, so the loop stays the same while your subgraph runs inside the full graph.

If composition fails (for example, a field conflict or a missing lookup), the orchestrator logs the error on the gateway console and the gateway fails to start. Every other resource keeps running, so you can fix the issue and restart the gateway, which composes again. You get the same composition validation as the Nitro CLI, integrated into your build step.

## Validating Changes Against the Stage

When `AddNitroComposition` binds the run to a stage and the gateway selects a Nitro API with `WithNitroApiId`, every composition is followed by a validation of the composed gateway schema against that stage. The AppHost uploads the schema to Nitro, and Nitro compares it with the schema version and the clients registered for the API and stage:

- Schema changes are classified as breaking, dangerous, or safe.
- Every registered client is checked with its persisted operations. A violated operation is reported with its hash, its deployed tags, and the message, code, path, and location of each error.

Validation is observational. The composed archive is installed before validation starts, so a finding never blocks the gateway, and the gateway serves the newly composed schema either way. Findings surface in two places:

- The gateway console logs the findings, grouped by client and operation.
- The Aspire dashboard shows a notification that names the gateway and the stage and links to the console logs. When a later composition passes again, a follow-up notification reports the recovery. A run that never violates anything triggers no notifications.

A composition that yields an unchanged gateway schema is not validated again, so a restart without schema changes sends no request to Nitro. Validation runs only while the AppHost runs; `aspire publish` never validates.

To turn validation off for a gateway:

<LanguageTabs>
<CSharp>

```csharp
builder
    .AddProject<Projects.Gateway>("gateway-api")
    .WithNitroComposition(disableValidation: true);
```

</CSharp>
<TypeScript>

```typescript
await builder
  .addProject("gateway-api", "../Gateway/Gateway.csproj")
  .withNitroComposition({ disableValidation: true });
```

</TypeScript>
</LanguageTabs>

Disabling validation turns off only the schema upload and the reports. Composition, the configuration download, and the archive install are unaffected.

# Next Steps

- **Need to compose without Aspire?** See the Nitro CLI composition workflow in [Adding a Subgraph](./adding-a-subgraph.md).
- **Need entity resolution patterns?** See [Entities and Lookups](./entities-and-lookups.md) for public vs. internal lookups, composite keys, and the node pattern.
- **Need cross-subgraph field dependencies?** See [Data Requirements](./data-requirements-and-mapping.md) for `@require` and FieldSelectionMap patterns.
- **Need visibility controls?** See [Schema Exposure and Evolution](./schema-exposure-and-evolution.md) for `@inaccessible`, `@internal`, `@deprecated`, `@requiresOptIn`, and `@override`.
- **Driving the run from the terminal?** [`aspire run`](https://aspire.dev/reference/cli/commands/aspire-run/) and [`aspire resource`](https://aspire.dev/reference/cli/commands/aspire-resource/) in the Aspire CLI reference apply to C# and TypeScript AppHosts alike.

---

### Website Content Docs Fusion Performance Tuning (`website/content/docs/fusion/performance-tuning.md`)

---
title: "Performance Tuning"
description: "Tune the Fusion gateway transport: configure the named fusion `HttpClient`, enable HTTP/2 for subgraph calls, and deduplicate identical in-flight requests."
---

The Fusion gateway proxies every GraphQL operation to one or more subgraphs over HTTP. The defaults work well out of the box, but high-throughput or latency-sensitive deployments can benefit from tuning the transport layer.

This page covers:

- Configuring the HTTP transport
- Enabling HTTP/2 for multiplexed subgraph communication
- Deduplicating identical in-flight requests
- Limiting concurrent request processing to maximize throughput

# Configure the HTTP Transport

Fusion uses a **named `HttpClient`** to communicate with subgraphs. The default client name is `"fusion"`, and you configure it through the standard `IHttpClientFactory` pattern. This gives you full control over connection behavior, timeouts, and message handlers.

A baseline `Program.cs` that registers the named client:

```csharp
var builder = WebApplication.CreateBuilder(args);

// 1. Register the named HTTP client for subgraph communication
builder.Services.AddHttpClient("fusion");

// 2. Configure the Fusion gateway
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far");

var app = builder.Build();
app.MapGraphQLHttp();
app.Run();
```

1. **Named HTTP client `"fusion"`**: the client the gateway uses to call subgraphs. Any handler configuration you add here applies to all subgraph requests.
2. **Gateway registration**: wires up the Fusion execution engine and loads the composed schema.

# HTTP/2

HTTP/2 multiplexes multiple requests over a single TCP connection, which reduces connection overhead when the gateway sends many concurrent requests to a subgraph. This is especially beneficial when subgraphs are behind a load balancer that supports HTTP/2.

## With TLS

When your subgraphs use TLS (HTTPS), HTTP/2 is negotiated automatically via ALPN. Enable `EnableMultipleHttp2Connections` to allow the gateway to open additional HTTP/2 connections when a single connection's stream limit is reached:

```csharp
builder.Services
    .AddHttpClient("fusion")
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
    {
        EnableMultipleHttp2Connections = true,
    });
```

No additional version configuration is needed. .NET negotiates HTTP/2 over TLS by default.

## Without TLS

In many Kubernetes deployments, services communicate over plaintext HTTP inside the cluster. HTTP/2 cleartext (h2c) requires explicit opt-in because .NET defaults to HTTP/1.1 for unencrypted connections.

To force HTTP/2 without TLS, set `DefaultRequestVersion` and `DefaultVersionPolicy` on the `HttpClient`:

```csharp
builder.Services
    .AddHttpClient("fusion", httpClient =>
    {
        httpClient.DefaultRequestVersion = HttpVersion.Version20;
        httpClient.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
    })
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
    {
        EnableMultipleHttp2Connections = true,
    });
```

The subgraph must also be configured to accept HTTP/2 over cleartext. By default, Kestrel only listens on HTTP/1.1 for non-TLS endpoints. Enable h2c in each subgraph's `Program.cs`:

```csharp
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenAnyIP(5001, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
    });
});
```

If you are unsure whether your infrastructure supports HTTP/2 cleartext end-to-end, **HTTP/1.1 works well** for most internal deployments. Switch to HTTP/2 only when you have confirmed support on both the gateway and all subgraphs.

# Request Deduplication

When multiple identical query requests are in flight to the same subgraph at the same time, **request deduplication** ensures only one HTTP request is actually sent. The first request becomes the "leader" and executes normally. Subsequent identical requests become "followers" that wait for the leader's response. Each caller receives an independent copy of the result.

## When It Helps

Deduplication is most effective when:

- **Burst traffic** hits the gateway with the same query. For example, a popular product page refreshing across many clients simultaneously.
- **Public APIs** serve unauthenticated traffic where many users send the same queries.
- **The same user** sends identical concurrent requests (e.g., a UI that fires duplicate fetches).

## Security Model

The deduplication hash includes the request body, URL, and the values of configurable **hash headers**. By default, `Authorization` and `Cookie` headers are included in the hash. This means:

- **Unauthenticated or public queries**: no auth headers, so identical queries from any client are deduplicated. High hit rate.
- **Same user, same query, concurrent**: identical tokens produce the same hash and are deduplicated.
- **Different users, same query**: different tokens produce different hashes. Never deduplicated. One user's response is never shared with another.

## How to Enable

Enabling deduplication takes two parts, and both are required. The gateway must annotate outgoing subgraph requests with the operation kind, and the HTTP client must carry the deduplication handler. Either part alone produces no deduplication.

First, opt in to operation kind annotation with `AnnotateOperationKind`:

```csharp
builder.Services
    .AddGraphQLGateway()
    .ModifyRequestOptions(options => options.AnnotateOperationKind = true);
```

This is off by default because materializing the annotation means building the request options bag on every subgraph call, and that allocation is only worth paying when something downstream consumes it. The operation kind is a general classification signal stored on `HttpRequestMessage.Options`, not a deduplication-only concept, so other delegating handlers further down the pipeline can read it too. Deduplication is one consumer of that signal.

Then add the request deduplication message handler with `.AddRequestDeduplication()` to the named HTTP client builder:

```csharp
builder.Services
    .AddHttpClient("fusion")
    .AddRequestDeduplication();
```

Without the gateway-level annotation, the handler has nothing to key off and passes all requests through unchanged.

## Customizing Hash Headers

By default, the `Authorization` and `Cookie` headers are included in the deduplication hash, which covers most setups. If you need additional headers to be part of the hash, for instance a tenant identifier in a multi-tenant application, add them to `HashHeaders`:

```csharp
.AddRequestDeduplication(options =>
{
    options.HashHeaders = ["Authorization", "Cookie", "X-Tenant-Id"];
});
```

For **service-to-service communication** where the gateway does not receive cookies, you can remove `Cookie` from the hash:

```csharp
.AddRequestDeduplication(options =>
{
    options.HashHeaders = ["Authorization"];
});
```

## What Gets Deduplicated

Only **query operations** are deduplicated. The following are **not** deduplicated:

- **Mutations**: not safe to coalesce because they have side effects.
- **Subscriptions**: long-lived connections that are inherently unique.
- **File uploads**: multipart requests bypass deduplication.

# Concurrent Execution Limiting

The gateway limits the number of simultaneous **executions** it processes using a **concurrency gate**. An execution is the work of running a single GraphQL operation end-to-end. Each query or mutation counts as one execution, and each event a subscription emits counts as one execution while its selection set runs. Capping concurrency keeps the gateway operating in its optimal throughput range. Too much work competing for the same resources (thread pool, memory, connections) can reduce overall throughput rather than increase it.

The default limit is **64 concurrent executions**. The default is calibrated for small containers 1 to 4 CPUs. Depending on your CPU count and typical operation cost, you may want to increase or decrease this value to find the optimal throughput for your hardware. The limit does not reject work; it queues it, and the GraphQL executor processes at most 64 executions concurrently by default.

Subscriptions participate in this limit like any other operation. Each event the gateway processes consumes a slot. Idle subscriptions between events cost nothing.

Set the limit through `ModifyServerOptions` on the gateway builder:

```csharp
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .ModifyServerOptions(options =>
    {
        options.MaxConcurrentExecutions = 128;
    });
```

You can override this limit for a specific HTTP endpoint using `WithOptions`:

```csharp
app.MapGraphQLHttp()
    .WithOptions(options =>
    {
        options.MaxConcurrentExecutions = 256;
    });
```

## Tuning Guidance

- **Too low**: executions queue behind the concurrency gate, adding latency even when the gateway and subgraphs have spare capacity. Subscriptions with high event rates feel this first.
- **Too high**: the gateway runs more work than it can efficiently process, leading to thread pool starvation and increased latency across queries, mutations, and subscription events.

Start with the default of 64 and adjust based on your workload. If you expect many long-lived subscriptions firing frequent events, factor those into your sizing. They now contend for the same slots as queries and mutations. Set to `null` to disable the limit entirely.

## Execution Cancellation

Every execution is bounded by the `ExecutionTimeout` option (default 30 seconds). This applies uniformly to queries, mutations, subscription handshakes, and each subscription event. The budget covers both the time an execution spends waiting for a concurrency slot and the time it spends running. When the budget is exceeded, the execution is cancelled and the caller receives a clean timeout error.

`ExecutionTimeout` is the single setting that controls cancellation for every execution. Configure it with `ModifyRequestOptions`:

```csharp
builder
    .AddGraphQLGateway()
    .ModifyRequestOptions(o => o.ExecutionTimeout = TimeSpan.FromSeconds(10));
```

If executions routinely time out at the gate, that is a signal to scale out or raise `MaxConcurrentExecutions`. Increasing `ExecutionTimeout` only defers the problem.

# Next Steps

- **"I want to cut the number of subgraph round trips"**: [Batching](./batching.md) covers variable, request, and alias batching, and the per-subgraph capabilities that select them.
- **"I need CDN and HTTP response caching behavior"**: [Cache Control](./cache-control.md) covers `@cacheControl`, composition merge behavior, and gateway response headers.
- **"I need to secure my gateway"**: [Authentication and Authorization](./authentication-and-authorization.md) covers JWT validation, header propagation, and subgraph-level authorization.
- **"I need to deploy this"**: [Deployment & CI/CD](./deployment-and-ci-cd.md) covers production deployment patterns and CI pipeline setup.
- **"I want to monitor performance"**: Observability and distributed tracing will be covered in future documentation.

---

### Website Content Docs Fusion Request Limits (`website/content/docs/fusion/request-limits.md`)

---
title: "Request Limits"
description: "Protect a Fusion gateway with request limits: parser options like `MaxAllowedFields`, validation depth caps, planner guardrails, and execution timeouts."
---

A Fusion gateway faces the same GraphQL query attacks as a standalone server: deep nesting, alias amplification, fragment expansion, and directive overloading. It also has gateway-specific risks like expensive query planning. Fusion enforces limits at every stage of the pipeline: parsing, validation, planning, and execution.

This page covers:

- Parser limits that reject payloads before the AST is fully constructed
- Validation limits that cap depth, cycles, and comparison work
- Planner guardrails that prevent expensive query plan generation
- Execution limits that bound time, request size, and transport features

# Parser Limits

Parser limits stop malicious payloads before the AST is fully constructed. They are the first line of defense because they run before any semantic analysis.

```csharp
builder
    .ModifyParserOptions(o =>
    {
        o.MaxAllowedFields = 1024;
        o.MaxAllowedDirectives = 4;
        o.MaxAllowedRecursionDepth = 100;
    });
```

| Option                     | Default        | Description                                                                                                  |
| -------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------ |
| `MaxAllowedFields`         | 2048           | Maximum number of fields allowed in a query document.                                                        |
| `MaxAllowedDirectives`     | 4 per location | Maximum number of directives allowed on a single location (field, operation, or fragment definition).        |
| `MaxAllowedRecursionDepth` | 200            | Maximum nesting depth the parser allows for selection sets, list values, object values, and type references. |
| `MaxAllowedNodes`          | Unlimited      | Maximum number of AST nodes the parser produces from a document. Defaults to unlimited.                      |
| `MaxAllowedTokens`         | Unlimited      | Maximum number of tokens the lexer processes. Defaults to unlimited.                                         |

# Validation Limits

After parsing, the validation pipeline enforces semantic limits on the query structure. These limits protect against queries that are syntactically valid but computationally expensive to execute.

## Execution Depth

Caps how deeply nested a query can be. A depth of 10 covers most real-world queries while blocking deeply nested attacks:

```csharp
builder.AddMaxExecutionDepthRule(10);
```

## Fragment Visits

Each time a visitor enters a fragment spread counts as one visit. Queries with deeply nested or repeated fragment spreads can cause exponential visitor work. The total number of fragment visits per operation is capped at **1,000** by default.

## Field Merge Comparisons

The overlapping-fields-can-be-merged rule caps comparison work at 100,000 by default. No configuration is needed for most gateways. The default protects against fragment expansion bombs.

## Field Coordinate Cycles

Some schemas contain self-referential relationships. For example, a `User` type with a `friends` field that returns `[User]`. Without a limit, a client can nest this relationship arbitrarily deep, causing resolver fan-out that grows exponentially with each level.

The field cycle depth rule tracks how many times each schema coordinate (e.g., `User.friends`) appears on the query path:

```csharp
builder.AddMaxAllowedFieldCycleDepthRule(defaultCycleLimit: 3);
```

With a limit of 3, the following query is valid:

```graphql
{
  user {
    friends {
      # User.friends — cycle 1
      friends {
        # User.friends — cycle 2
        friends {
          # User.friends — cycle 3
          name
        }
      }
    }
  }
}
```

Adding a fourth level of `friends` would be rejected.

You can override the limit for specific coordinates:

```csharp
builder.AddMaxAllowedFieldCycleDepthRule(
    defaultCycleLimit: 3,
    coordinateCycleLimits:
    [
        (new SchemaCoordinate("Category", "parent"), 10),
    ]);
```

This rule is enabled by default in non-development environments as part of the default security policy. You can remove it with `RemoveMaxAllowedFieldCycleDepthRule()` if your schema does not contain self-referential relationships.

## Validation Errors

Caps the total number of validation errors reported per request. The default is 5. When the limit is reached, validation stops early instead of continuing to accumulate errors.

```csharp
builder.SetMaxAllowedValidationErrors(5);
```

## Introspection Depth

Introspection queries with recursive fields like `__Type.ofType` or `__Type.fields` can be used to construct expensive queries that consume significant server resources. The concern is not schema discovery (the schema is available at `/graphql/schema.graphql` by default when using `MapGraphQL`, computed once with no performance impact), but resource consumption from deeply recursive introspection operations.

```csharp
builder.SetIntrospectionAllowedDepth(
    maxAllowedOfTypeDepth: 8,
    maxAllowedListRecursiveDepth: 1);
```

## Disable Introspection

Introspection is disabled in non-development environments by default as part of the default security policy. This prevents clients from running expensive introspection queries against production systems. To disable it unconditionally (including in development):

```csharp
builder.DisableIntrospection();
```

# Operation Planner Guardrails

Before execution, the gateway plans how to distribute the query across subgraphs. Complex queries can cause expensive planning. These guardrails prevent planning from consuming excessive resources.

```csharp
builder
    .ModifyPlannerOptions(o =>
    {
        o.MaxPlanningTime = TimeSpan.FromSeconds(5);
        o.MaxExpandedNodes = 10_000;
        o.MaxQueueSize = 5_000;
    });
```

| Option                           | Default  | Description                                                                  |
| -------------------------------- | -------- | ---------------------------------------------------------------------------- |
| `MaxPlanningTime`                | Disabled | Maximum wall-clock time allowed for generating a query plan.                 |
| `MaxExpandedNodes`               | Disabled | Maximum number of planner nodes that may be expanded during plan generation. |
| `MaxQueueSize`                   | Disabled | Maximum size of the planner's internal work queue.                           |
| `MaxGeneratedOptionsPerWorkItem` | Disabled | Maximum number of options the planner generates per work item.               |

# Execution Limits

## Timeout

Requests are aborted after 30 seconds by default. The timeout covers the entire request including all subgraph calls. It is not enforced when a debugger is attached.

```csharp
builder
    .ModifyRequestOptions(o =>
    {
        o.ExecutionTimeout = TimeSpan.FromSeconds(10);
    });
```

## HTTP Request Size

The maximum HTTP request body size defaults to approximately 20 MB:

```csharp
builder.AddGraphQLGateway(maxAllowedRequestSize: 5 * 1000 * 1024); // 5 MB
```

A JSON request body, or the `operations` field of a multipart request, over the limit is rejected before parsing with the error code `HC0010`. Under the `Draft20260903` [transport version](../hotchocolate/server/http-transport.md#transport-versions), the response has a `413` status code.

## Server Options

Control which HTTP methods and features are available:

```csharp
builder
    .ModifyServerOptions(o =>
    {
        o.EnableMultipartRequests = false;
    });
```

# Next Steps

- **"I need to limit operation cost."** [Cost Analysis](./cost-analysis.md) covers field-cost, type-cost, and response-size limits before operation planning.
- **"I need to secure my gateway."** [Authentication and Authorization](./authentication-and-authorization.md) covers JWT validation, header propagation, and subgraph-level authorization.
- **"I need to tune transport performance."** [Performance Tuning](./performance-tuning.md) covers HTTP/2, request deduplication, and concurrency limiting.
- **"I need CDN and HTTP response caching behavior."** [Cache Control](./cache-control.md) covers `@cacheControl`, composition merge behavior, and gateway response headers.

---

### Website Content Docs Fusion Schema Exposure And Evolution (`website/content/docs/fusion/schema-exposure-and-evolution.md`)

---
title: "Schema Exposure and Evolution"
description: "Control client visibility and schema evolution in Fusion with @inaccessible, @internal, @deprecated, @requiresOptIn, and @override field migration."
---

Not everything in your source schema should be visible to clients, and not everything should stay the same forever. As your distributed graph grows, you need control over two things: what clients can see today, and how the schema changes over time.

This page covers five directives that handle exposure and evolution. `@inaccessible` and `@internal` control visibility in the composite schema. `@deprecated` and `@requiresOptIn` manage the lifecycle of fields and values. `@override` migrates field ownership between subgraphs. If you have completed the [Getting Started](./getting-started.md) tutorial and worked through [Entities and Lookups](./entities-and-lookups.md), you already used `@internal` on lookup fields. Here, you will see the full picture of visibility control and schema evolution.

# Controlling Client Visibility

Your source schemas contain fields and types that serve different audiences. Some are for clients, some carry internal data shared between subgraphs, and some are infrastructure that only the gateway uses. Fusion provides two directives for hiding schema elements from the composite schema. They differ in how they interact with composition merging.

## Hidden Fields

Mark a field or type `@inaccessible` to hide it from the public client-facing composite schema while keeping it available for internal. The element still participates in composition merging and can be referenced by `@require` dependencies in other subgraphs.

**GraphQL schema**

```graphql
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
  internalSkuCode: Int! @inaccessible
}
```

**C# declaration**

```csharp
[EntityKey("id")]
public class Product
{
    public int Id { get; set; }

    public required string Name { get; set; }

    public double Price { get; set; }

    [Inaccessible]
    public int InternalSkuCode { get; set; }
}
```

Clients cannot query `internalSkuCode`. But other subgraphs can depend on it through `[Require]`. For example, a Warehouse subgraph could require the SKU code for inventory lookups without exposing it to clients.

Apart from `@require` inaccessible fields can also be used as lookups or as keys.

You can apply `@inaccessible` to fields, types, arguments, enum values, input fields, scalars, interfaces, and unions. Any schema element that can appear in the composite schema can be hidden.

**Enum values**

Hiding individual enum values is useful when different subgraphs define the same enum with slightly different values. Mark the values that should not be in the composite schema as `@inaccessible` to resolve merge conflicts.

```graphql
enum OrderStatus {
  PENDING
  SHIPPED
  DELIVERED
  CANCELLED @inaccessible
}
```

The `CANCELLED` value does not appear in the composite schema. Subgraphs can still return it internally, but clients never see it.

**Constraint:** You cannot mark a required input field as `@inaccessible`. If a client must provide a value, they need to see the field. Composition fails if you try.

## Internal Lookups

The `@internal` directive is designed for lookups. An internal lookup is a query field that the gateway uses for entity resolution but that clients cannot call. Internal lookups do not participate in composition merging, which means multiple subgraphs can define lookups with the same field name and different argument shapes without causing a conflict. This gives each subgraph the flexibility to resolve an entity in whatever way makes sense for its data, without coordinating field signatures across teams.

**GraphQL schema**

```graphql
type Query {
  productById(id: ID!): Product @lookup @internal
}
```

**C# resolver**

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

Without `[Internal]`, this lookup would appear in the composite schema as a second `productById` query field, conflicting with the Products subgraph's public lookup. With `[Internal]`, the gateway can still use it for entity resolution, but clients never see it.

You can also group internal lookups under a dedicated root object to keep routing infrastructure in one place.

**GraphQL schema (grouped internal lookups)**

```graphql
type Query {
  internalLookups: InternalLookups @internal
}

type InternalLookups @internal {
  productByTenantAndSku(tenantId: ID!, sku: String!): Product @lookup
}
```

**C# declaration**

```csharp filename="Reviews/Types/InternalLookups.cs"
[QueryType]
public static partial class Query
{
    [Internal]
    public static InternalLookups GetInternalLookups { get; } = new();
}

[Internal, ObjectType]
public partial class InternalLookups
{
    [Lookup]
    public Product? GetProductByTenantAndSku(int tenantId, string sku)
        => ProductRepository.GetByTenantAndSku(tenantId, sku);
}
```

For a deeper look at internal vs. public lookups, composite keys, and the node pattern, see [Entities and Lookups](./entities-and-lookups.md).

## Choosing Between Hidden and Internal

These directives serve different purposes. `@inaccessible` hides data from clients while keeping it available across subgraphs. `@internal` keeps lookups local to one subgraph so they can vary freely without merge conflicts.

| Behavior                          | `@inaccessible`                        | `@internal`                         |
| --------------------------------- | -------------------------------------- | ----------------------------------- |
| Visible to clients                | No                                     | No                                  |
| Participates in merging           | Yes                                    | No                                  |
| Can conflict across subgraphs     | Yes (types must be compatible)         | No                                  |
| Usable in `@require` dependencies | Yes                                    | No                                  |
| Primary use case                  | Internal data shared between subgraphs | Lookup entry points for the gateway |

Use `@inaccessible` when the field carries data that other subgraphs need but clients should not see. Use `@internal` on lookups that exist only for gateway entity resolution.

# Deprecating Fields, Values, and Types

The `@deprecated` directive signals that a field, argument, or enum value is being phased out. Clients see the deprecation reason in introspection, and GraphQL tooling (IDEs, linters, code generators) can warn consumers to migrate away. The field continues to work. Deprecation is a soft signal, not a hard removal.

**GraphQL schema**

```graphql
type Query {
  product(id: ID!): Product
    @lookup
    @deprecated(reason: "Use `productById` instead.")
  productBySku(sku: String!): Product @lookup
}
```

**C# resolver**

```csharp filename="Products/Types/ProductQueries.cs"
[QueryType]
public static partial class ProductQueries
{
    [GraphQLDeprecated("Use `productBySku` instead.")]
    [Lookup]
    public static async Task<Product?> GetProductAsync(
        int id,
        IProductByIdDataLoader productById,
        CancellationToken cancellationToken)
        => await productById.LoadAsync(id, cancellationToken);

    [Lookup]
    public static async Task<Product?> GetProductBySkuAsync(
        string sku,
        IProductBySkuDataLoader productBySku,
        CancellationToken cancellationToken)
        => await productBySku.LoadAsync(id, cancellationToken);
}
```

You can also use .NET's built-in `[Obsolete]` attribute. Hot Chocolate treats it the same as `[GraphQLDeprecated]` for fields, arguments, input fields, and enum values. Deprecating an object type itself is different: see [Deprecating Object Types](#deprecating-object-types).

```csharp
[Obsolete("Use `productById` instead.")]
[Lookup]
public static async Task<Product?> GetProductAsync(...)
    => ...;
```

Deprecation applies to output fields, input fields, arguments, and enum values without any extra configuration. Object types can be deprecated too, but only once you enable a separate opt-in option (see [Deprecating Object Types](#deprecating-object-types)).

**Enum value deprecation**

```graphql
enum SortOrder {
  ASC
  DESC
  RELEVANCE @deprecated(reason: "Use full-text search instead.")
}
```

**Constraint:** You cannot deprecate a non-null argument or input field without a default value. If clients must provide a value, they cannot stop using the field.

## Deprecating Object Types

Object type deprecation is not yet part of the released GraphQL specification. It tracks [graphql-spec RFC #997](https://github.com/graphql/graphql-spec/pull/997), which is still open, so its final shape could still change. It is disabled by default; enable it separately on each subgraph and on the Fusion gateway.

**Subgraph configuration**

```csharp filename="Products/Program.cs"
builder
    .AddGraphQL("Products")
    .AddTypes()
    .ModifyOptions(o => o.EnableObjectDeprecation = true);
```

**Gateway configuration**

```csharp filename="Gateway/Program.cs"
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .ModifyOptions(o => o.EnableObjectDeprecation = true);
```

When `EnableObjectDeprecation` is enabled on the gateway, the introspection schema exposes `isDeprecated` and `deprecationReason` on `__Type`, plus the `includeDeprecated` argument on `__schema.types` and `__Type.possibleTypes`. Deprecated object types are hidden from both by default; clients pass `includeDeprecated: true` to see them.

**GraphQL schema**

```graphql
type Query {
  animals: [Animal]
}

interface Animal {
  name: String
}

type Dog implements Animal {
  name: String
}

type Baiji implements Animal @deprecated(reason: "No longer known to exist.") {
  name: String
}
```

**C# declaration**

```csharp
[GraphQLDeprecated("No longer known to exist.")]
public class Baiji
{
    public string? Name { get; set; }
}
```

The .NET `[Obsolete]` attribute does not deprecate an object type; only `[GraphQLDeprecated]` on the class does. Unifying the two attributes for object types is deferred to a future major version.

A field that is not itself deprecated cannot return a deprecated object type. Here, `Query.animals` returns the interface `Animal`, not `Baiji` directly, so the subgraph's schema is valid. A field that returns `Baiji` directly would need to be deprecated too:

```graphql
type Query {
  baiji: Baiji
}

type Baiji @deprecated(reason: "No longer known to exist.") {
  name: String
}
```

**Constraint:** `Query.baiji` is not deprecated but returns the deprecated `Baiji` type, so building the subgraph's schema fails. Deprecate the field, or change its return type.

A deprecated object type remains a valid union member and interface implementation. Only the field returning it is checked.

## Deprecation Across Subgraphs

If a shareable field is deprecated in at least one subgraph, it is deprecated in the composite schema. You do not need to deprecate it in every subgraph that defines it. With shared ownership comes the power for any owner to deprecate the field for all clients.

If you only want to remove a shared field from one subgraph, you do not need to deprecate it. Remove the field from that subgraph and the gateway will resolve it from the remaining subgraphs that still provide it.

Object types follow the same rule. If at least one subgraph deprecates a type, it is deprecated in the composite schema, and the reason is taken from the first subgraph that provides one.

# Experimental and Preview Features

The `@requiresOptIn` directive is the counterpart to `@deprecated`. Where `@deprecated` signals that a field is going away, `@requiresOptIn` signals that a field is not yet stable. Fields marked with `@requiresOptIn` are hidden from introspection by default. Clients must explicitly opt in to discover and use them.

This is useful for rolling out experimental features, expensive operations, or anything where the consumer should make a conscious decision before using it.

**GraphQL schema**

```graphql
type Product {
  id: ID!
  name: String!
  price: Float!
  dynamicPrice: Decimal @requiresOptIn(feature: "experimentalPricing")
}
```

**C# declaration**

```csharp filename="Products/Types/Product.cs"
public class Product
{
    public int Id { get; set; }

    public required string Name { get; set; }

    public double Price { get; set; }

    [RequiresOptIn("experimentalPricing")]
    public decimal? DynamicPrice { get; set; }
}
```

The `dynamicPrice` field does not appear in standard introspection results. Clients must opt in to see it.

The directive is repeatable. A single field can be part of multiple features.

```csharp
[RequiresOptIn("experimentalPricing")]
[RequiresOptIn("betaApi")]
public decimal? DynamicPrice { get; set; }
```

## Enabling Opt-In Support

Opt-in features are disabled by default. You must enable them separately on each subgraph and on the Fusion gateway.

**Subgraph configuration**

```csharp filename="Products/Program.cs"
builder
    .AddGraphQL("Products")
    .AddTypes()
    .ModifyOptions(o => o.EnableOptInFeatures = true);
```

**Gateway configuration**

```csharp filename="Gateway/Program.cs"
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .ModifyOptions(o => o.EnableOptInFeatures = true);
```

When `EnableOptInFeatures` is enabled on the gateway, the introspection schema exposes the `includeOptIn` argument and `__schema.optInFeatures` / `optInFeatureStability` fields. Members marked `@requiresOptIn` are hidden from introspection unless the client opts into their feature via `includeOptIn`. Opt-in affects introspection visibility only: hidden members remain fully executable, and the gateway never rejects a request that selects an opt-in field.

## Discovering Opt-In Fields

Clients pass the `includeOptIn` argument in introspection queries to discover opt-in fields.

```graphql
{
  __type(name: "Product") {
    fields(includeOptIn: ["experimentalPricing"]) {
      name
      requiresOptIn
    }
  }
}
```

The `includeOptIn` argument is available on `fields`, `args`, `inputFields`, and `enumValues` in introspection queries.

To discover which opt-in features exist in the schema:

```graphql
{
  __schema {
    optInFeatures
  }
}
```

## Feature Stability Levels

You can declare the stability level of each opt-in feature at the schema level. This lets consumers know whether a feature is experimental, preview, or any other stability level you define.

**C# configuration**

```csharp filename="Products/Program.cs"
builder
    .AddGraphQL("Products")
    .AddTypes()
    .ModifyOptions(o => o.EnableOptInFeatures = true)
    .OptInFeatureStability("experimentalPricing", "experimental");
```

Consumers can query feature stability through introspection:

```graphql
{
  __schema {
    optInFeatureStability {
      feature
      stability
    }
  }
}
```

## Constraints

Like `@deprecated`, you cannot apply `@requiresOptIn` to non-null arguments or input fields without a default value. Hiding a required field would break queries that need to provide it.

## Opt-In Across Subgraphs

If a shareable field is marked `@requiresOptIn` in at least one subgraph, it requires opt-in in the composite schema. To make the field generally available again, every subgraph that defines it must remove the `@requiresOptIn` directive. This is the inverse of `@deprecated`, where a single subgraph can deprecate a field for all clients. With `@requiresOptIn`, a single subgraph can gate a shared field behind opt-in, and it stays gated until all owners agree to remove the restriction.

## End-to-End Flow

The full lifecycle of an opt-in feature across a Fusion deployment is:

1. A subgraph marks a field (or enum value, or argument) with `@requiresOptIn(feature: "featureName")` and enables opt-in support with `#!csharp ModifyOptions(o => o.EnableOptInFeatures = true)`.
2. Composition merges `@requiresOptIn` by union: if a shareable member is opt-in in any source schema, it is opt-in in the execution schema.
3. The gateway, configured with `EnableOptInFeatures` enabled, hides opt-in members from introspection by default. Clients that pass `includeOptIn: ["featureName"]` in their introspection query see those members.
4. Opt-in members are fully executable regardless of whether the client opted in at the introspection level. The gateway does not reject execution-time requests for opt-in fields.

## Stability Mismatch Across Subgraphs

When multiple subgraphs declare stability for the same opt-in feature, they must agree. If two source schemas declare different stability values for the same feature name, composition fails with `OPT_IN_FEATURE_STABILITY_MISMATCH`. Align the `OptInFeatureStability` call for that feature across all subgraphs that declare it.

# Migrating Field Ownership Between Subgraphs

As your system evolves, you may need to move a field from one subgraph to another. A team might split a subgraph, or a field might belong more naturally in a different domain. The `@override` directive migrates field ownership without breaking existing queries.

When you apply `#!csharp [Override(from: "source-subgraph")]`, the gateway routes requests for that field to the new subgraph instead of the original. The old subgraph's resolver is no longer called. No client-facing changes are needed.

**Before: Products subgraph owns the reviews field**

```csharp
[ObjectType<Product>]
public static partial class ProductNode
{
    public static async Task<IEnumerable<Review>> GetReviewsAsync(
        [Parent] Product product,
        ReviewService reviewService)
        => await reviewService.GetReviewsByProductIdAsync(product.Id);
}
```

**After: Reviews subgraph takes ownership**

```csharp
[ObjectType<Product>]
public static partial class ProductNode
{
    [Override(from: "products-api")]
    public static async Task<Connection<Review>> GetReviewsAsync(
        [Parent] Product product,
        PagingArguments args,
        IReviewsByProductIdDataLoader loader,
        CancellationToken ct)
        => await loader
            .With(args)
            .LoadAsync(product.Id, ct)
            .ToConnectionAsync();
}
```

**GraphQL schema**

```graphql
# Reviews subgraph
type Product {
  id: ID!
  reviews: [Review!]! @override(from: "products-api")
}
```

The `from` argument is the subgraph name (from `schema-settings.json`) that originally owned the field.

## Migration Workflow

1. Add the field to the new subgraph with `#!csharp [Override(from: "old-subgraph")]`.
2. Export schemas and compose. Composition validates that the override is valid.
3. Deploy the new subgraph. The gateway routes the field to it.
4. Remove the old resolver from the original subgraph when ready.

The old resolver stays in place during the transition. Both subgraphs can define the field simultaneously because `[Override]` tells composition which one wins. This avoids duplicate-field errors without requiring `[Shareable]`.

# Next Steps

- **Need entity resolution patterns?** See [Entities and Lookups](./entities-and-lookups.md) for public vs. internal lookups, composite keys, and the node pattern.
- **Need cross-subgraph field dependencies?** See [Data Requirements](./data-requirements-and-mapping.md) for `@require`, `@is`, and FieldSelectionMap patterns.
- **Need field sharing and ownership rules?** See [Field Ownership](./field-ownership-and-sharing.md) for `@shareable`, `@external`, and `@provides` patterns.
- **Adding a new subgraph?** See [Adding a Subgraph](./adding-a-subgraph.md) for the full walkthrough of creating and composing a new subgraph.

---

### Website Content Docs Fusion Source Schema Extensions (`website/content/docs/fusion/source-schema-extensions.md`)

---
title: "Source Schema Extensions"
description: "Add Fusion directives like @lookup and @inaccessible to schemas you cannot edit using a source schema extensions document layered over the base SDL."
---

Some source schemas are not yours to edit, or you want to keep them free of Fusion-specific annotations. Fusion still needs [directives](./directives-reference.md) like `@lookup`, `@internal`, and `@inaccessible` on those schemas to compose your graph correctly. A **source schema extensions document** solves this. It is a separate SDL file you author yourself, layered over the base schema at parse time, that adds Fusion-specific annotations without modifying a single character of the base. Your original schema stays clean, and the directives the gateway needs to plan distributed queries land in a file you fully control.

# How It Works

Both documents (the base schema and its extensions) are parsed sequentially into the same schema model. Extensions written with `#!sdl extend type X` merge into the canonical `type X` definition during parsing. By the time composition runs, your extensions are indistinguishable from directives written directly on the base schema. Composition never sees the `extend` syntax: the parser has already folded everything into one source schema.

```mermaid
flowchart LR
    A[products.graphqls] --> P[Parser]
    B[products-extensions.graphqls] --> P
    P --> S[Single merged source schema]
    S --> C[Composition]
    C --> E[Execution schema]
```

# A Realistic Example

Suppose you compose a federated graph from several services. One of them is a Products service that exposes `products.graphqls` as its source schema, and you want to keep that file free of Fusion-specific annotations. You need to mark `productById` as a lookup, expose a second lookup that Reviews uses for hydration without making it part of the public surface, hide an internal `warehouseLocationCode` field, and add a derived `stockStatus` field that depends on the warehouse code.

## The Base Schema

```graphql
type Query {
  productById(id: ID!): Product
  productByCode(code: String!): Product
  searchProducts(query: String!): [Product!]!
}

type Product {
  id: ID!
  name: String!
  description: String
  price: Money!
  warehouseLocationCode: String
}

type Money {
  amount: Int!
  currency: String!
}
```

There is not a single Fusion annotation in this file. It can change without forcing edits to your Fusion-specific configuration. Every Fusion-specific decision lives in the next document.

## The Extensions Document

```graphql
extend type Query {
  productById(id: ID!): Product @lookup
  productByCode(code: String!): Product @lookup @internal
}

extend type Product {
  warehouseLocationCode: String @inaccessible
  stockStatus(
    warehouseLocationCode: String @require(field: "warehouseLocationCode")
  ): StockStatus!
}

enum StockStatus {
  IN_STOCK
  OUT_OF_STOCK
  LIMITED
}
```

What each block does:

- `#!sdl extend type Query { productById(id: ID!): Product @lookup }` promotes an existing field to a lookup. The field name targets the existing field, and the return type must match the base. Arguments do not need to be repeated; if you do repeat them, their types and defaults must match the base. New arguments may be added.
- `#!sdl extend type Query { productByCode(code: String!): Product @lookup @internal }` is also a lookup, but `@internal` keeps it out of the public surface. The gateway uses it to enter the Products subgraph when resolving cross-subgraph references, while clients never see it. See [Entities and Lookups](./entities-and-lookups.md) for the public versus internal lookup distinction.
- `#!sdl extend type Product { warehouseLocationCode: String @inaccessible }` applies a directive to an existing field. The field type repeats the base declaration; only the directive is the new contribution. Hidden fields can still be referenced by `@require` dependencies in other source schemas. See [Schema Exposure and Evolution](./schema-exposure-and-evolution.md).
- `#!sdl extend type Product { stockStatus(warehouseLocationCode: ... @require(...)): StockStatus! }` adds a brand new field with a hidden resolver argument. `#!sdl @require(field: "warehouseLocationCode")` tells the gateway to populate the argument from the existing `warehouseLocationCode` field, and the argument is removed from the public surface so clients see `stockStatus: StockStatus!` with no arguments. Adding a field via extensions still requires that field to be resolvable at runtime by the underlying subgraph implementation: the extensions document only declares the field, it does not provide the resolver. See [Data Requirements](./data-requirements-and-mapping.md) for `@require` semantics.
- `#!sdl enum StockStatus { ... }` introduces a new type. Extensions can declare types the base schema does not, because extensions are valid SDL documents in their own right.

## The Merged Result

After parsing, the source schema looks like this from composition's perspective:

```graphql
type Query {
  productById(id: ID!): Product @lookup
  productByCode(code: String!): Product @lookup @internal
  searchProducts(query: String!): [Product!]!
}

type Product {
  id: ID!
  name: String!
  description: String
  price: Money!
  warehouseLocationCode: String @inaccessible
  stockStatus(
    warehouseLocationCode: String @require(field: "warehouseLocationCode")
  ): StockStatus!
}

enum StockStatus {
  IN_STOCK
  OUT_OF_STOCK
  LIMITED
}
```

This is the same shape you would have if you had hand-edited the base schema. The difference is purely in how you got there: two files, parsed in order, with the second contributing only the deltas.

# What You Can Do with an Extensions Document

**Apply directives to existing fields.** This is the most common use. Directives commonly applied this way include `@lookup`, `@internal`, `@inaccessible`, `@shareable`, `@external`, `@provides`, `@override`, `@deprecated`, and `@requiresOptIn`. For the semantics of each, see [Entities and Lookups](./entities-and-lookups.md), [Field Ownership](./field-ownership-and-sharing.md), and [Schema Exposure and Evolution](./schema-exposure-and-evolution.md).

```graphql
extend type Product {
  internalSku: String @inaccessible
}
```

**Apply directives to field arguments.** Some directives, notably `@require`, are defined on argument definitions and attach to arguments rather than fields. See [Data Requirements](./data-requirements-and-mapping.md) for the full pattern.

```graphql
extend type Product {
  shippingEstimate(weight: Float @require(field: "weight")): Int!
}
```

**Apply directives to types themselves.**

```graphql
extend type LegacyOrder @inaccessible
```

**Add new fields to existing types.** The new field needs a runtime resolver in the subgraph for the gateway to actually fetch it. Declaring the field in extensions only adds it to the source schema model.

```graphql
extend type Product {
  displayName: String!
}
```

**Introduce new types or enums the base schema does not declare.** Rare, but supported, because extensions are valid SDL documents in their own right and the parser merges them into the same schema model.

```graphql
enum StockStatus {
  IN_STOCK
  OUT_OF_STOCK
  LIMITED
}
```

# Using Extensions

## File Convention

Place the extensions file alongside the base schema using the `{name}-extensions.graphqls` naming convention. The Fusion CLI loads it automatically. There is no extra flag and no configuration entry to add.

```text
schemas/
  products.graphqls
  products-extensions.graphqls
  reviews.graphqls
  reviews-extensions.graphqls
```

When `nitro fusion compose` runs, each base schema and its matching extensions document are parsed together into a single source schema before composition starts.

## Inside the Fusion Archive

The archive preserves both files separately. The base schema is stored at `source-schemas/{name}/schema.graphqls`, and the extensions document is stored at `source-schemas/{name}/schema-extensions.graphqls`.

The base schema is stored exactly as supplied. Fusion does not fold the extensions into it. This matters because recomposition (when a subgraph's schema changes and the archive is rebuilt) needs the extensions document to reapply your annotations on top of the new base. Without it, the directive layer would be lost on every regeneration. Keeping the two files distinct in the archive is what makes "the base schema stays untouched" hold end-to-end, not only in your repo but in every downstream consumer.

# Behavior Reference

| Situation                                                       | Behavior                                                                                              |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `#!sdl extend type X` references a type not in the base schema  | The type is implicitly created.                                                                       |
| Extension declares a field that already exists on the base type | Directives merge onto the existing field. Return type and any provided arguments must match the base. |
| Same field appears twice in one `extend` block                  | Parsing fails with a duplicate field error.                                                           |
| Extension applies a directive the schema does not define        | Implicit at parse time. Fusion's built-ins compose as-is; other custom directives must be declared.   |
| Extension re-applies a non-repeatable directive to the target   | The parser keeps both applications. Composition then fails validation.                                |

# Troubleshooting

**My directive did not apply.** Check the field name. If it does not exactly match a field on the base type, the parser adds a new field rather than extending the existing one, and the directive lands on that new field. Return type or repeated argument mismatches are different and cause parsing to fail rather than silently producing a new field.

**Composition reports the field is already defined.** You wrote a bare `type X` redeclaration where you meant `#!sdl extend type X`, and the field was duplicated. Add the `extend` keyword.

**I want to apply a directive that the schema does not import.** Fusion's built-in directives compose without further declaration. For any other custom directive, declare it in the schema (or in the extensions document) before applying it; otherwise composition will fail validation even though the parser accepts the unknown directive.

# Next Steps

- [Composition](./composition.md): what happens after extensions are folded in.
- [Adding a Subgraph](./adding-a-subgraph.md): the typical entry point that may use extensions.
- [Schema Exposure and Evolution](./schema-exposure-and-evolution.md): directives commonly applied via extensions.

---

### Website Content Docs Fusion Subscriptions (`website/content/docs/fusion/subscriptions.md`)

---
title: "Subscriptions"
description: "How Fusion federates GraphQL subscriptions with broker-backed Federated Event Streams and SSE subgraph subscriptions."
---

GraphQL subscriptions let clients receive a continuous stream of results whenever
something changes on the server. In a federated graph, a subscription is more
involved than a query or a mutation: the stream is long-lived, the events usually
originate deep inside your backend, and the data a client selects often spans
several subgraphs.

Fusion supports two complementary models for subscriptions:

1. **Federated Event Streams.** The subscription is backed by
   a message broker (NATS, Kafka, Azure Event Hubs, Amazon SQS, or Redis).
   The gateway subscribes to a broker topic, and for every event it
   resolves the requested fields across your subgraphs with
   ordinary stateless fetches. This is the recommended model for scale, because the
   gateway holds no per-subscription state of its own.
2. **Subgraph subscriptions over Server-Sent Events (SSE).** The subscription is
   implemented by a single subgraph as a normal GraphQL subscription, and the
   gateway consumes that stream over SSE. This is the simplest
   model when a single subgraph already owns the event source.

The first half of this page covers federated event streams, including
client-resumable streams. The second half covers GraphQL over SSE subgraph subscriptions.

# Federated Event Streams

Federated event streams decouple the event source from the GraphQL schema. Your
services publish events to a message broker; the gateway subscribes to the relevant
topics and turns each event into a fully resolved GraphQL result.

<FusionSubscriptionsDiagram />

The flow has four moving parts:

1. **A client opens a long-lived subscription** against the gateway (over WebSockets
   or SSE, exactly like a non-federated subscription).
2. **The gateway subscribes to a topic** on the broker (NATS calls topics "subjects").
   The gateway does not open a subscription against any subgraph for this field.
3. **Your services publish events** to that topic. An event payload is small: it
   carries just the data the gateway needs to resolve the rest of the selection set,
   typically an entity key such as `#!json { "id": "1" }`.
4. **The gateway resolves each event** by running ordinary stateless fetches against
   the owning subgraphs (the same entity lookups it uses for queries) and emits one
   GraphQL result per event to the client.

Because the gateway only holds a broker subscription (not a stateful pipeline), the
gateway stays horizontally scalable, and durability and ordering are delegated to
the broker.

## Declaring an event stream

An event stream is a subscription root field annotated with the `@eventStream`
directive on the subgraph that owns the event.

The field's return type is the **event payload**. It does not have to be an entity: it
can be a plain type that carries pure event data, an entity whose current state the
gateway resolves, or a payload that mixes event data with links to entities. The
`message` argument is the bridge: it is a selection set over the return type that names
exactly the fields the broker message delivers, and the gateway resolves anything else
the client selects across your subgraphs. The example below returns the `Product`
entity, so the message only needs its key; [Event payloads](#event-payloads) covers the
other shapes.

```graphql
# Products subgraph
type Subscription {
  onProductPriceChanged(productId: ID!): Product @eventStream(message: "{ id }")
}

type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
}
```

The `@eventStream` directive takes three arguments:

| Argument  | Type                 | Description                                                                                                                                               |
| --------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `message` | `FieldSelectionSet!` | Required. The shape of the event payload, expressed as a selection set over the field's return type.                                                      |
| `topics`  | `[String!]`          | The broker topic(s) to subscribe to, with optional `{$args.<name>}` templates. When omitted, the topic is inferred from the field name and its arguments. |
| `broker`  | `String`             | The name of the registered broker. When omitted, the default (unnamed) broker is used.                                                                    |

The `message` selection set tells the gateway which fields the broker payload
contains. Here `#!graphql { id }` means an event body of `#!json { "id": "1" }`. The gateway uses that
key to resolve everything else the client asked for.

Because `topics` and `broker` are omitted, Fusion infers the topic from the field name
and its arguments (`onProductPriceChanged-{$args.productId}`) and uses the default
broker. Set them explicitly to override: see [Topics](#topics) and
[Connecting a message broker](#connecting-a-message-broker).

In Hot Chocolate you author the same field with the `[EventStream]` attribute. The
directive that ends up in your schema is `@eventStream`; the `EventStream` naming is
the C# surface for it.

```csharp
public class Subscriptions
{
    [EventStream("{ id }")]
    public Product OnProductPriceChanged(string productId)
        => EventStream.Create<Product>(productId);
}
```

Or with the fluent API:

```csharp
public class Subscriptions
{
    public Product OnProductPriceChanged(string productId)
        => EventStream.Create<Product>(productId);
}

public class SubscriptionType : ObjectType<Subscriptions>
{
    protected override void Configure(IObjectTypeDescriptor<Subscriptions> descriptor)
    {
        descriptor.Name("Subscription");

        descriptor
            .Field(f => f.OnProductPriceChanged(default!))
            .EventStream("{ id }");
    }
}
```

> [!NOTE]
> The resolver body of an event-stream field never runs; the gateway fulfills
> these fields from the broker, not a local resolver. `EventStream.Create<T>` is a
> compile-time placeholder that always throws. Pass the field's arguments to it so
> analyzers do not flag them as unused.

A client subscribes to the field like any other subscription:

```graphql
subscription {
  onProductPriceChanged(productId: "1") {
    name
    price
  }
}
```

Notice that the client selects `name` and `price`, even though the event payload
only carries `id`. Because `onProductPriceChanged` returns the `Product`
[entity](./entities-and-lookups.md), the gateway resolves the remaining
fields the same way it resolves a federated query.
A client can even select fields owned by other subgraphs, for example `reviews` from a
Reviews subgraph, and the gateway fetches them per event:

```graphql
subscription {
  onProductPriceChanged(productId: "1") {
    name
    price
    reviews {
      body
    }
  }
}
```

## Event payloads

Returning an entity, as above, is only one option. The return type can be any payload
that suits the event, and `message` always lists the fields the broker delivers.

When the event is self-contained, use a plain payload type. The message selects every
field, and no subgraph is queried per event:

```graphql
# Products subgraph
type Subscription {
  onProductPriceChanged(productId: ID!): ProductPriceChangedEvent
    @eventStream(message: "{ productId oldPrice newPrice }")
}

type ProductPriceChangedEvent {
  productId: ID!
  oldPrice: Float!
  newPrice: Float!
}
```

To combine event data with live, cross-subgraph data, give the payload a field that
links to an entity and include that entity's key in the message:

```graphql
# Products subgraph
type Subscription {
  onProductPriceChanged(productId: ID!): ProductPriceChangedEvent
    @eventStream(message: "{ oldPrice newPrice product { id } }")
}

type ProductPriceChangedEvent {
  oldPrice: Float!
  newPrice: Float!
  product: Product!
}
```

Now `oldPrice` and `newPrice` come straight from the broker message, while `product` is
resolved across your subgraphs from the `#!graphql { product { id } }` key, so a client reads both
the event data and live entity fields in one event:

```graphql
subscription {
  onProductPriceChanged(productId: "1") {
    oldPrice
    newPrice
    product {
      name
      reviews {
        body
      }
    }
  }
}
```

## Connecting a message broker

Brokers are registered on the gateway builder returned by `AddGraphQLGateway()`. Most
gateways use a single broker, which you register without a name to make it the default.
Fields then need no `broker` argument.

### NATS

Install the `HotChocolate.Fusion.Subscriptions.NATS` package and register the broker:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddHttpClient("fusion");

builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .AddNatsEventStreamBroker(options =>
    {
        options.Url = "nats://localhost:4222";
    });

var app = builder.Build();

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

### Kafka

Install the `HotChocolate.Fusion.Subscriptions.Kafka` package and register the broker:

```csharp
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .AddKafkaEventStreamBroker(options =>
    {
        options.BootstrapServers = "localhost:9092";
        options.AutoOffsetReset = AutoOffsetReset.Earliest;
    });
```

Kafka gives every GraphQL subscriber its own consumer group, so each client receives
the full stream rather than competing for partitions. SASL and SSL options are
available on `KafkaEventStreamOptions` for secured clusters.

`AutoOffsetReset` controls where a subscription without a resume cursor starts:
`Latest` (default) delivers only future events, while `Earliest` replays from the
earliest retained event. For cursor-enabled subscriptions, the same setting determines
where a fresh subscribe begins, and the emitted cursor reflects that start position.

For cursor-enabled subscriptions, the broker seeds initial partition positions before
accepting the first event. Tune `SeedingQueryTimeout` (maximum time per partition
lookup, default 2 s) and `SeedingDeadline` (overall budget for all partitions, default
5 s). If any partition position is still missing when the deadline expires, the
subscription fails rather than starting with an incomplete cursor.

The default message channel waits when full, which keeps cursor-based resume lossless.
Use `KafkaEventStreamOptions.CreateBoundedMessageChannel` with a drop mode for
at-most-once delivery, where a resume skips any messages dropped under backpressure.

### Azure Event Hubs

Install the `HotChocolate.Fusion.Subscriptions.AzureEventHubs` package and register the
broker. Each subscribed topic is treated as an Event Hub name, so the topic the gateway
resolves for a field (see [Topics](#topics)) must match a hub in the namespace.

Authenticate with a connection string:

```csharp
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .AddAzureEventHubsEventStreamBroker(options =>
    {
        options.ConnectionString = "<event-hubs-connection-string>";
    });
```

Or with a fully qualified namespace and a token credential. When you set
`FullyQualifiedNamespace` without supplying a `Credential`, the broker uses
`DefaultAzureCredential`:

```csharp
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .AddAzureEventHubsEventStreamBroker(options =>
    {
        options.FullyQualifiedNamespace = "my-namespace.servicebus.windows.net";
        options.Credential = new DefaultAzureCredential();
    });
```

Every GraphQL subscriber reads independently, so each client receives the full stream.
`StartFromEarliest` controls where a subscription without a resume cursor starts: omit
it (default) to begin at the latest event, or set it to `true` to replay from the
earliest retained event. For cursor-enabled subscriptions, the same setting determines
where a fresh subscribe begins, and the emitted cursor reflects that start position.

A cursor-tracked subscription opens one reader per (hub, partition) pair. Event Hubs
allows at most 5 concurrent non-exclusive readers per partition per consumer group,
which is about 5 concurrent cursor-tracked subscribers per hub on one group. Use
separate `ConsumerGroup` values to scale beyond that limit.

For cursor-enabled subscriptions, the broker seeds initial partition positions before
accepting the first event. Tune `SeedingQueryTimeout` (maximum time per partition
metadata lookup, default 2 s) and `SeedingDeadline` (overall budget for all partitions,
default 5 s). If any partition position is still missing when the deadline expires, the
subscription fails rather than starting with an incomplete cursor.

`PartitionDiscoveryInterval` (default 30 s) controls how often a running cursor-tracked
subscription checks for partitions added after it started. Raise or lower this based
on how quickly you expect partition scale-out to be visible to subscribers.

The default message channel waits when full, which keeps cursor-based resume lossless.
Use `AzureEventHubsEventStreamOptions.CreateBoundedMessageChannel` with a drop mode for
at-most-once delivery, where a resume skips any messages dropped under backpressure.

### Amazon SQS

Install the `HotChocolate.Fusion.Subscriptions.AmazonSqs` package and register the
broker. On real AWS, the region and the default AWS credential chain are usually all you
need:

```csharp
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .AddAmazonSqsEventStreamBroker(options =>
    {
        options.Region = "us-east-1";
    });
```

Set `Credentials` to supply explicit AWS credentials, or `ServiceUrl` to point at a
LocalStack or other SQS-compatible endpoint:

```csharp
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .AddAmazonSqsEventStreamBroker(options =>
    {
        options.ServiceUrl = "http://localhost:4566";
        options.Region = "us-east-1";
        options.Credentials = new BasicAWSCredentials("test", "test");
    });
```

The gateway creates a dedicated SQS queue per active subscription and deletes it when
the subscription ends. To broadcast one event to every subscriber, configure
`ResolveTopicArn` so each generated queue is subscribed to the SNS topic for the logical
Fusion topic:

```csharp
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .AddAmazonSqsEventStreamBroker(options =>
    {
        options.Region = "us-east-1";
        options.ResolveTopicArn = topic => topic switch
        {
            "product.price-changed" => "arn:aws:sns:us-east-1:123456789012:product-price-changed",
            _ => null
        };
    });
```

Without `ResolveTopicArn`, the broker runs in direct queue mode: publishers must send to
the generated SQS queue URLs themselves. That mode is useful for controlled integration
tests and custom topologies, but it is not SNS fan-out.

The queue name is derived from the logical topic using `QueueNamePrefix`. Tune
`WaitTimeSeconds` (long-poll wait), `MaxNumberOfMessages` (messages per receive), and
`VisibilityTimeoutSeconds` (raise it for slow consumers to avoid duplicate delivery) on
`AmazonSqsEventStreamOptions`. The broker uses standard queues and consumes at-least-once,
so design resolvers to tolerate the occasional duplicate event. SQS does not retain an
ordered history, so SQS-backed streams are not [resumable](#client-resumable-subscriptions).

### Redis

Install the `HotChocolate.Fusion.Subscriptions.Redis` package and register the broker.
Each subscribed topic is treated as a Redis Pub/Sub channel name:

```csharp
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .AddRedisEventStreamBroker(options =>
    {
        options.Configuration = "localhost:6379";
    });
```

Redis Pub/Sub broadcasts every message to all connected subscribers, so each GraphQL
subscriber receives the full stream. For finer control over the connection, set
`ConfigurationOptions` (which takes precedence over `Configuration`), or supply an
existing `ConnectionMultiplexer` that the broker shares but does not dispose:

```csharp
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .AddRedisEventStreamBroker(options =>
    {
        options.ConnectionMultiplexer = ConnectionMultiplexer.Connect("localhost:6379");
    });
```

Redis Pub/Sub does not retain message history, so Redis-backed streams are not
[resumable](#client-resumable-subscriptions).

### Using multiple brokers

To run more than one broker, give each a name and select one per field with the
`broker` argument:

```csharp
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .AddNatsEventStreamBroker("nats", o => o.Url = "nats://localhost:4222")
    .AddKafkaEventStreamBroker("kafka", o => o.BootstrapServers = "localhost:9092");
```

```graphql
type Subscription {
  onProductPriceChanged(productId: ID!): Product
    @eventStream(message: "{ id }", broker: "nats")
}
```

## Publishing events

The gateway only consumes events. Your services publish them out-of-band using the
broker's native client. An event body must contain at least the fields named in the
field's `message` selection set.

With NATS:

```csharp
await using var connection = new NatsConnection(
    new NatsOpts { Url = "nats://localhost:4222" });

var payload = JsonSerializer.SerializeToUtf8Bytes(new { id = "1" });

await connection.PublishAsync("onProductPriceChanged-1", payload);
```

With Kafka:

```csharp
using var producer = new ProducerBuilder<Null, byte[]>(
    new ProducerConfig { BootstrapServers = "localhost:9092" })
    .Build();

var payload = JsonSerializer.SerializeToUtf8Bytes(new { id = "1" });

await producer.ProduceAsync(
    "onProductPriceChanged-1",
    new Message<Null, byte[]> { Value = payload });

producer.Flush();
```

With Amazon SQS, publish to the SNS topic that the gateway's `ResolveTopicArn` maps the
logical topic to. SNS fans the message out to every subscribed queue:

```csharp
using var sns = new AmazonSimpleNotificationServiceClient();

var payload = JsonSerializer.Serialize(new { id = "1" });

await sns.PublishAsync(
    "arn:aws:sns:us-east-1:123456789012:product-price-changed",
    payload);
```

In direct queue mode (no `ResolveTopicArn`), there is no shared topic to publish to;
you send to the per-subscription queue URLs yourself, which is mainly useful for tests.

With Redis, publish to the channel that matches the field's topic. Redis fans the
message out to every subscribed gateway:

```csharp
var multiplexer = await ConnectionMultiplexer.ConnectAsync("localhost:6379");
var subscriber = multiplexer.GetSubscriber();

var payload = JsonSerializer.Serialize(new { id = "1" });

await subscriber.PublishAsync(
    RedisChannel.Literal("onProductPriceChanged-1"),
    payload);
```

## Topics

By default Fusion infers the topic from the field name and its arguments, joined with
hyphens: `#!sdl onProductPriceChanged(productId: ID!)` infers `onProductPriceChanged-{$args.productId}`,
and a field with two arguments infers `<fieldName>-{$args.arg1}-{$args.arg2}`, and so on.

Set `topics` explicitly when you need a different name. A topic can contain
`{$args.<name>}` placeholders that the gateway expands against the arguments a client
supplies when it subscribes:

```graphql
type Subscription {
  onProductPriceChanged(productId: ID!): Product
    @eventStream(
      message: "{ id }"
      topics: ["product.price-changed.{$args.productId}"]
    )
}
```

A client subscribing with `#!graphql onProductPriceChanged(productId: "1")` is wired to the
topic `product.price-changed.1`, so it only receives the events relevant to that
product. This keeps fan-out at the broker rather than in the gateway.

> [!NOTE]
> `{$args.<name>}` is the placeholder. To include a literal brace in a topic,
> double it: `{{` produces `{` and `}}` produces `}`. To wrap a value in literal braces,
> escape the outer pair, so `topic-{{{$args.productId}}}` resolves to `topic-{1}` (for
> `productId: "1"`), and `topic-{{{{{$args.productId}}}}}` resolves to `topic-{{1}}`.

# Client-resumable subscriptions

A long-lived subscription will, sooner or later, be interrupted: a client loses
connectivity, a mobile app is suspended, or the gateway is redeployed. Resumable
subscriptions let a client pick up exactly where it left off, without missing events
and without re-receiving events it has already processed.

Resumption is driven by an opaque **cursor**. The broker assigns each event a cursor;
the client stores the most recent one and, on reconnect, passes it back to resume the
stream from immediately after that event.

Kafka and Azure Event Hubs use a composite cursor that encodes one position per
assigned (topic, partition) pair, so those brokers support resumable streams even
when a subscription field resolves to multiple topics. A NATS JetStream cursor is a
single sequence number within the configured stream; because every subscribed subject
is filtered from that same stream, multi-topic fields resume there as well.

Two markers wire this up, both expressed with the `@eventCursor` directive:

- An argument marked `@eventCursor` accepts the resume cursor.
- An output field marked `@eventCursor` carries the cursor of each emitted event so
  the client can store it.

To carry a cursor, the subscription returns a small event payload type that holds the
changed entity together with the cursor field.

```graphql
# Products subgraph
type Subscription {
  onProductPriceChanged(
    productId: ID!
    after: String @eventCursor
  ): ProductPriceChange @eventStream(message: "{ product { id } }")
}

type ProductPriceChange {
  product: Product!
  cursor: String @eventCursor
}
```

In Hot Chocolate, mark the resume argument with `[EventCursor]` and the cursor
property with `[property: EventCursor]`:

```csharp
public class Subscriptions
{
    [EventStream("{ product { id } }")]
    public ProductPriceChange OnProductPriceChanged(
        [EventCursor] string? after,
        string productId)
        => EventStream.Create<ProductPriceChange>(after, productId);
}

public record ProductPriceChange(
    Product Product,
    [property: EventCursor] string Cursor);
```

The fluent equivalent marks the argument with `.EventCursor()`:

```csharp
descriptor
    .Field(f => f.OnProductPriceChanged(default, default!))
    .EventStream("{ product { id } }")
    .Argument("after", a => a.EventCursor());
```

## How a client resumes

On the initial subscription, the client selects the cursor field and stores the value
from each event:

```graphql
subscription {
  onProductPriceChanged(productId: "1") {
    product {
      name
      price
    }
    cursor
  }
}
```

```json
// event 1
{ "data": { "onProductPriceChanged": { "product": { "name": "Gadget", "price": 9.99 }, "cursor": "Y3Vyc29yLTE=" } } }
// event 2
{ "data": { "onProductPriceChanged": { "product": { "name": "Gadget", "price": 8.49 }, "cursor": "Y3Vyc29yLTI=" } } }
```

After a disconnect, the client re-issues the same subscription and passes the last
cursor it received as the `@eventCursor` argument:

```graphql
subscription {
  onProductPriceChanged(productId: "1", after: "Y3Vyc29yLTE=") {
    product {
      name
      price
    }
    cursor
  }
}
```

The stream resumes strictly after that event: in this example the client receives
event 2 onward and never re-receives event 1. A first-time subscriber simply omits
the argument.

The cursor is an opaque, base64-encoded token. Its meaning is owned by the broker (a
JetStream sequence for NATS; a composite of one `topic:partition:offset` entry per
assigned partition for Kafka; a composite of one `hub:partition:sequenceNumber` entry
per assigned (hub, partition) pair for Azure Event Hubs), so clients should
treat it as a black box and never parse or construct it.

> [!NOTE]
> Resumable streams need a broker that retains and orders history. Configure
> NATS with JetStream, or use Kafka or Azure Event Hubs. Core NATS pub/sub does not keep
> history, so it cannot resume; Amazon SQS deletes each event once delivered; and Redis
> Pub/Sub does not retain history either, so SQS- and Redis-backed streams cannot resume.

For NATS, enable JetStream when registering the broker:

```csharp
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far")
    .AddNatsEventStreamBroker(options =>
    {
        options.Url = "nats://localhost:4222";
        options.JetStream = new NatsJetStreamOptions
        {
            Stream = "products"
        };
    });
```

If a client passes a cursor the broker cannot honor, the field resolves to `null` with
an error, and no broker subscription is opened:

```json
{
  "errors": [
    {
      "message": "The cursor is invalid.",
      "path": ["onProductPriceChanged"]
    }
  ],
  "data": { "onProductPriceChanged": null }
}
```

# Subscriptions over Server-Sent Events

Not every subscription needs a broker. When a single subgraph already implements a
GraphQL subscription, the gateway can federate it directly by consuming that
subgraph's stream over Server-Sent Events (SSE) or JSON Lines.

This is fetch-based: the gateway sends an HTTP request to the subgraph and reads the
streamed response. There is no WebSocket connection between the gateway and the
subgraph.

## Serving SSE from a subgraph

A Hot Chocolate subgraph serves subscriptions over SSE automatically. There is no
SSE-specific switch to flip. Define a subscription type, register a subscription
provider, and map the GraphQL endpoint:

```csharp
builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddSubscriptionType<Subscription>()
    .AddInMemorySubscriptions(); // or .AddRedisSubscriptions(...)

var app = builder.Build();

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

The GraphQL HTTP endpoint negotiates the transport from the `Accept` header. When a
caller requests `text/event-stream`, the server streams the result using the
[graphql-sse](https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md)
protocol. You can verify it with curl:

```shell
curl -N \
  -H 'Content-Type: application/json' \
  -H 'Accept: text/event-stream' \
  -d '{"query":"subscription { onReviewAdded { body } }"}' \
  http://localhost:5000/graphql
```

```text
event: next
data: {"data":{"onReviewAdded":{"body":"Great product"}}}

event: next
data: {"data":{"onReviewAdded":{"body":"Works as described"}}}

event: complete
data:
```

For details on defining subscriptions in a subgraph (the `[Subscribe]` and `[Topic]`
attributes, `ITopicEventSender`, and the in-memory, Redis, NATS, and Postgres
providers), see
[Hot Chocolate subscriptions](../hotchocolate/defining-a-schema/subscriptions.md).

## Configuring the subgraph transport

By default the gateway advertises both JSON Lines and SSE when it subscribes to a
subgraph (sending `Accept: application/jsonl, text/event-stream`), and the subgraph's
response content type decides which is used. You can control this per subgraph in its
settings under the HTTP transport's subscription capability:

```json
{
  "name": "Reviews",
  "transports": {
    "http": {
      "url": "http://reviews/graphql",
      "capabilities": {
        "subscriptions": {
          "supported": true,
          "formats": ["text/event-stream"]
        }
      }
    }
  }
}
```

Set `supported` to `false` to tell the gateway a subgraph does not serve
subscriptions, or restrict `formats` to pin a single transport.

> [!NOTE]
> Subgraph subscriptions are only supported through the default HTTP
> connector. Subgraphs integrated through the Apollo Federation connector cannot serve
> subscriptions.

## Choosing between event streams and SSE

Both models expose a normal GraphQL subscription to clients. They differ in how the
gateway sources events:

- **Federated event streams** source events from a message broker and resolve the
  selection set with stateless fetches. Prefer them when events fan out to many
  subscribers, when you need durability or resumable streams, or when the event
  source is not a subgraph.
- **SSE subgraph subscriptions** source events from a single subgraph that implements
  the subscription itself. Prefer it when one subgraph already owns the event source
  and you do not need a broker.

# Directive reference

For the full SDL of `@eventStream` and `@eventCursor`, including the composed output,
see the [Directive Reference](./directives-reference.md).

---

### Website Content Docs Hotchocolate Get Started With Graphql In Net Core (`website/content/docs/hotchocolate/get-started-with-graphql-in-net-core.md`)

---
title: "Getting Started with GraphQL in .NET"
metaTitle: "GraphQL .NET Tutorial with Hot Chocolate"
description: "In this tutorial, you will walk through the basics of creating a GraphQL server with Hot Chocolate."
---

import { InputChoiceTabs } from "../../../components/mdx/input-choice-tabs"

By the end of this guide, you will have a running GraphQL server that responds to queries. You will use the Hot Chocolate project template, explore the generated code, and execute your first query in the Nitro GraphQL IDE.

**Prerequisites:** [.NET 8 SDK](https://dotnet.microsoft.com/download) or later.

# Create the Project

Install the Hot Chocolate templates.

```shell
dotnet new install HotChocolate.Templates
```

Create a new project from the template.

<InputChoiceTabs>
<InputChoiceTabs.CLI>

```shell
dotnet new graphql --name GettingStarted
```

This creates a `GettingStarted` directory with the project files. Open it in your editor.

</InputChoiceTabs.CLI>
<InputChoiceTabs.VisualStudio>

Create a new project from within Visual Studio using the **GraphQL Server** template.

[Learn how to create a new project in Visual Studio](https://docs.microsoft.com/visualstudio/ide/create-new-project)

</InputChoiceTabs.VisualStudio>
</InputChoiceTabs>

# Explore the Generated Code

## Domain Types

The `Types` directory contains two record types that represent the domain model.

```csharp
public record Author(string Name);
```

```csharp
public record Book(string Title, Author Author);
```

These are regular C# types. Hot Chocolate infers the GraphQL schema from them.

## Query Type

The `Query` class defines the root type for read operations. Each public method becomes a field that clients can query.

```csharp
[QueryType]
public static partial class Query
{
    public static Book GetBook()
        => new Book("C# in depth.", new Author("Jon Skeet"));
}
```

The `[QueryType]` attribute tells the source generator to register this class as part of the GraphQL Query type. The class must be `partial` so the source generator can add the registration code at build time.

The method `GetBook` becomes a field named `book` in the schema. Hot Chocolate strips the `Get` prefix by convention.

## Program.cs

The generated `Program.cs` sets up the server.

```csharp
builder.AddGraphQL()
```

`AddGraphQL` returns an `IRequestExecutorBuilder` for configuring the GraphQL server. The template also calls a source-generated `AddTypes` method that registers all types decorated with attributes like `[QueryType]` in the current assembly.

```csharp
app.MapGraphQL()
```

`MapGraphQL` exposes the GraphQL endpoint at `/graphql`. This is where clients send queries and where Nitro (the built-in GraphQL IDE) is served.

```csharp
app.RunWithGraphQLCommands(args)
```

`RunWithGraphQLCommands` works like `Run()` but adds developer commands. For example, you can export the schema as SDL.

```shell
dotnet run -- schema export
```

This writes a `schema.graphqls` file to your project directory.

# Run the Server

<InputChoiceTabs>
<InputChoiceTabs.CLI>

```shell
dotnet run
```

</InputChoiceTabs.CLI>
<InputChoiceTabs.VisualStudio>

Press `Ctrl + F5` or click the green **Debug** button in the toolbar.

</InputChoiceTabs.VisualStudio>
</InputChoiceTabs>

If everything worked, the terminal output includes a line like this:

```text
Now listening on: http://localhost:5095
```

Open [http://localhost:5095/graphql](http://localhost:5095/graphql) in your browser. You should see the Nitro GraphQL IDE.



Click **Create Document**, verify the HTTP Endpoint matches your server URL, and click **Apply**.



You should see the editor with **Schema available** at the bottom right.



# Execute a Query

Paste the following query into the **Request** pane.

```graphql
{
  book {
    title
    author {
      name
    }
  }
}
```

Click **Run**. The **Response** pane should show:

```json
{
  "data": {
    "book": {
      "title": "C# in depth.",
      "author": {
        "name": "Jon Skeet"
      }
    }
  }
}
```



You can browse the schema by clicking the **Schema** tab next to **Operation**. The **Schema Definition** tab shows the raw SDL.



Your GraphQL server is running and responding to queries.

# Next Steps

- **"I want to learn about the type system."** See [Defining a Schema](./defining-a-schema/index.md) for queries, mutations, subscriptions, and all the GraphQL types.

- **"I want to fetch data from a database."** See [DataLoader](./fetching-data/batching/dataloader.md) for batched data fetching, or [Entity Framework](./fetching-data/integrations/entity-framework.md) for EF Core integration.

- **"I want a deeper tutorial."** Check out the [GraphQL Workshop](https://github.com/ChilliCream/graphql-workshop) for a hands-on walkthrough covering types, resolvers, DataLoaders, filtering, and more.

- **"I'm new to GraphQL."** Read the [official GraphQL introduction](https://graphql.org/learn/) to understand the concepts before diving deeper into Hot Chocolate.

---

### Website Content Docs Mocha Diagnostics (`website/content/docs/mocha/diagnostics.md`)

---
title: "Diagnostics"
description: "Reference for all compile-time diagnostics emitted by the Mocha source generator, including causes, examples, and fixes for each warning and error."
---

Mocha uses a Roslyn source generator to validate your message handlers, consumers, and sagas at compile time. When the generator detects a problem - a missing handler, a duplicate registration, an invalid type - it emits a diagnostic that appears as a compiler warning or error in your IDE and build output. You can fix these issues before your code ever runs.

# Quick reference

| Code              | Description                                              | Severity |
| ----------------- | -------------------------------------------------------- | -------- |
| [MO0001](#mo0001) | Missing handler for message type                         | Warning  |
| [MO0002](#mo0002) | Duplicate handler for message type                       | Error    |
| [MO0003](#mo0003) | Handler is abstract                                      | Warning  |
| [MO0004](#mo0004) | Open generic message type cannot be dispatched           | Info     |
| [MO0005](#mo0005) | Handler implements multiple mediator handler interfaces  | Error    |
| [MO0006](#mo0006) | Open generic handler cannot be auto-registered           | Info     |
| [MO0011](#mo0011) | Duplicate handler for request type                       | Error    |
| [MO0012](#mo0012) | Open generic messaging handler cannot be auto-registered | Info     |
| [MO0013](#mo0013) | Messaging handler is abstract                            | Warning  |
| [MO0014](#mo0014) | Saga must have a public parameterless constructor        | Error    |
| [MO0015](#mo0015) | Missing JsonSerializerContext for AOT                    | Error    |
| [MO0016](#mo0016) | Missing JsonSerializable attribute                       | Error    |
| [MO0018](#mo0018) | Type not in JsonSerializerContext                        | Warning  |
| [MO0020](#mo0020) | Command/query sent but no handler found                  | Warning  |

# Mediator diagnostics

These diagnostics apply to the in-process [mediator](./mediator/index.md) - commands, queries, and notifications dispatched within a single process.

## MO0001

**Missing handler for message type**

|              |                                                |
| ------------ | ---------------------------------------------- |
| **Severity** | Warning                                        |
| **Message**  | `Message type '{0}' has no registered handler` |

### Cause

A command or query type is declared but no corresponding handler implementation exists. The mediator requires exactly one handler for each command and query type. This diagnostic does not apply to notifications, which can have zero handlers.

### Example

```csharp
using Mocha.Mediator;

// Command with no handler - triggers MO0001
public record PlaceOrder(Guid OrderId, decimal Total) : ICommand;
```

### Fix

Implement a handler for the message type.

```csharp
using Mocha.Mediator;

public record PlaceOrder(Guid OrderId, decimal Total) : ICommand;

public class PlaceOrderHandler : ICommandHandler<PlaceOrder>
{
    public ValueTask HandleAsync(
        PlaceOrder command,
        CancellationToken cancellationToken)
    {
        // process the order
        return ValueTask.CompletedTask;
    }
}
```

## MO0002

**Duplicate handler for message type**

|              |                                                 |
| ------------ | ----------------------------------------------- |
| **Severity** | Error                                           |
| **Message**  | `Message type '{0}' has multiple handlers: {1}` |

### Cause

A command or query type has more than one handler implementation. Commands and queries require exactly one handler - the mediator cannot decide which one to call. This diagnostic does not apply to notifications, which support multiple handlers by design.

### Example

```csharp
using Mocha.Mediator;

public record PlaceOrder(Guid OrderId, decimal Total) : ICommand;

// Two handlers for the same command - triggers MO0002
public class PlaceOrderHandler : ICommandHandler<PlaceOrder>
{
    public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct)
        => ValueTask.CompletedTask;
}

public class DuplicateOrderHandler : ICommandHandler<PlaceOrder>
{
    public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct)
        => ValueTask.CompletedTask;
}
```

### Fix

Remove all but one handler. If you need multiple side effects for the same action, consider publishing a notification from the single handler and reacting to it with separate notification handlers.

```csharp
using Mocha.Mediator;

public record PlaceOrder(Guid OrderId, decimal Total) : ICommand;

public class PlaceOrderHandler : ICommandHandler<PlaceOrder>
{
    public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct)
        => ValueTask.CompletedTask;
}
```

## MO0003

**Handler is abstract**

|              |                                                        |
| ------------ | ------------------------------------------------------ |
| **Severity** | Warning                                                |
| **Message**  | `Handler '{0}' is abstract and will not be registered` |

### Cause

A class implements a [handler](./handlers-and-consumers.md) interface (`ICommandHandler`, `IQueryHandler`, or `INotificationHandler`) but is declared `abstract`. The source generator skips abstract types because they cannot be instantiated.

### Example

```csharp
using Mocha.Mediator;

public record GetOrderTotal(Guid OrderId) : IQuery<decimal>;

// Abstract handler - triggers MO0003
public abstract class GetOrderTotalHandler : IQueryHandler<GetOrderTotal, decimal>
{
    public abstract ValueTask<decimal> HandleAsync(
        GetOrderTotal query,
        CancellationToken cancellationToken);
}
```

### Fix

Make the handler concrete. If you want shared base logic, move it to a base class that does not implement the handler interface, and have the concrete handler extend it.

```csharp
using Mocha.Mediator;

public record GetOrderTotal(Guid OrderId) : IQuery<decimal>;

public class GetOrderTotalHandler : IQueryHandler<GetOrderTotal, decimal>
{
    public ValueTask<decimal> HandleAsync(
        GetOrderTotal query,
        CancellationToken cancellationToken)
    {
        return ValueTask.FromResult(99.99m);
    }
}
```

## MO0004

**Open generic message type cannot be dispatched**

|              |                                                                             |
| ------------ | --------------------------------------------------------------------------- |
| **Severity** | Info                                                                        |
| **Message**  | `Message type '{0}' is an open generic and cannot be dispatched at runtime` |

### Cause

A command or query type has unbound type parameters. The mediator dispatches concrete types at runtime and cannot resolve an open generic like `MyCommand<T>`.

### Example

```csharp
using Mocha.Mediator;

// Open generic command - triggers MO0004
public record ProcessItem<T>(T Item) : ICommand;
```

### Fix

Use concrete message types instead.

```csharp
using Mocha.Mediator;

public record ProcessOrder(Guid OrderId) : ICommand;
public record ProcessPayment(decimal Amount) : ICommand;
```

## MO0005

**Handler implements multiple mediator handler interfaces**

|              |                                                                       |
| ------------ | --------------------------------------------------------------------- |
| **Severity** | Error                                                                 |
| **Message**  | `Handler '{0}' must implement exactly one mediator handler interface` |

### Cause

A single class implements more than one of `ICommandHandler`, `IQueryHandler`, or `INotificationHandler`. Each handler class must implement exactly one mediator handler interface so the generator can produce unambiguous registrations.

### Example

```csharp
using Mocha.Mediator;

public record PlaceOrder(Guid OrderId) : ICommand;
public record GetOrder(Guid OrderId) : IQuery<Order>;

// Implements both command and query handler - triggers MO0005
public class OrderHandler
    : ICommandHandler<PlaceOrder>,
      IQueryHandler<GetOrder, Order>
{
    public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct)
        => ValueTask.CompletedTask;

    public ValueTask<Order> HandleAsync(GetOrder query, CancellationToken ct)
        => ValueTask.FromResult(new Order());
}
```

### Fix

Split into separate handler classes, one per interface.

```csharp
using Mocha.Mediator;

public record PlaceOrder(Guid OrderId) : ICommand;
public record GetOrder(Guid OrderId) : IQuery<Order>;

public class PlaceOrderHandler : ICommandHandler<PlaceOrder>
{
    public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct)
        => ValueTask.CompletedTask;
}

public class GetOrderHandler : IQueryHandler<GetOrder, Order>
{
    public ValueTask<Order> HandleAsync(GetOrder query, CancellationToken ct)
        => ValueTask.FromResult(new Order());
}
```

## MO0006

**Open generic handler cannot be auto-registered**

|              |                                                                                                                                                                                       |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Severity** | Info                                                                                                                                                                                  |
| **Message**  | `Handler '{0}' has unbound type parameters; source-generated registration is skipped. Register the closed form manually (e.g. AddHandler<ConcreteHandler<MyType>>()) if intentional.` |

### Cause

A mediator handler (`ICommandHandler<T>`, `ICommandHandler<T, TResponse>`, `IQueryHandler<T, TResponse>`, or `INotificationHandler<T>`) is declared with unbound type parameters. This diagnostic fires on the handler _declaration_ - the source generator cannot produce a registration for an open generic because there is no closed type to register.

Under AOT, this matters because every dispatched message must correspond to a statically-known handler type; the runtime cannot close the generic for you.

### Example

```csharp
using Mocha.Mediator;

public record MyNotification : INotification;

// Open generic handler - triggers MO0006
public class GenericNotificationHandler<T> : INotificationHandler<MyNotification>
{
    public ValueTask HandleAsync(
        MyNotification notification,
        CancellationToken ct)
        => ValueTask.CompletedTask;
}
```

### Fix

Either close the generic by declaring a concrete handler, or register the closed form manually and suppress the diagnostic on the declaration.

Close the generic at the handler declaration:

```csharp
using Mocha.Mediator;

public record MyNotification : INotification;

public class MyNotificationHandler : INotificationHandler<MyNotification>
{
    public ValueTask HandleAsync(
        MyNotification notification,
        CancellationToken ct)
        => ValueTask.CompletedTask;
}
```

Or, if the open generic is intentional (for example, a reusable handler body parameterized on a secondary type), register the closed form manually and suppress MO0006 on the declaration:

```csharp
using Mocha.Mediator;

public record MyNotification : INotification;

#pragma warning disable MO0006
public class GenericNotificationHandler<T> : INotificationHandler<MyNotification>
{
    public ValueTask HandleAsync(
        MyNotification notification,
        CancellationToken ct)
        => ValueTask.CompletedTask;
}
#pragma warning restore MO0006

// Register the closed form explicitly:
// services.AddMediator(m => m.AddHandler<GenericNotificationHandler<MyConcreteType>>());
```

# Messaging diagnostics

These diagnostics apply to the [message bus](./handlers-and-consumers.md) - event handlers, request handlers, batch handlers, consumers, and sagas that communicate across service boundaries.

## MO0011

**Duplicate handler for request type**

|              |                                                 |
| ------------ | ----------------------------------------------- |
| **Severity** | Error                                           |
| **Message**  | `Request type '{0}' has multiple handlers: {1}` |

### Cause

A request type (used with `SendAsync` or `RequestAsync`) has more than one [handler](./handlers-and-consumers.md) implementation. Request types require exactly one handler - the bus cannot route to multiple targets.

### Example

```csharp
using Mocha;

public record ProcessPayment(decimal Amount);

// Two handlers for the same request type - triggers MO0011
public class PaymentHandlerA : IEventRequestHandler<ProcessPayment>
{
    public ValueTask HandleAsync(
        ProcessPayment request,
        CancellationToken ct)
        => ValueTask.CompletedTask;
}

public class PaymentHandlerB : IEventRequestHandler<ProcessPayment>
{
    public ValueTask HandleAsync(
        ProcessPayment request,
        CancellationToken ct)
        => ValueTask.CompletedTask;
}
```

### Fix

Keep one handler per request type.

```csharp
using Mocha;

public record ProcessPayment(decimal Amount);

public class ProcessPaymentHandler : IEventRequestHandler<ProcessPayment>
{
    public ValueTask HandleAsync(
        ProcessPayment request,
        CancellationToken ct)
        => ValueTask.CompletedTask;
}
```

## MO0012

**Open generic messaging handler cannot be auto-registered**

|              |                                                                  |
| ------------ | ---------------------------------------------------------------- |
| **Severity** | Info                                                             |
| **Message**  | `Handler '{0}' is an open generic and cannot be auto-registered` |

### Cause

A messaging handler (`IEventHandler<T>`, `IEventRequestHandler<T>`, `IBatchEventHandler<T>`, or `IConsumer<T>`) has unbound type parameters. The source generator cannot produce registration code for open generic types.

### Example

```csharp
using Mocha;

// Open generic handler - triggers MO0012
public class GenericEventHandler<T> : IEventHandler<T>
{
    public ValueTask HandleAsync(
        T message,
        CancellationToken ct)
        => ValueTask.CompletedTask;
}
```

### Fix

Make the handler concrete. If you need to handle multiple event types with shared logic, create a concrete handler for each type and extract the shared logic into a base class or shared service.

If you need to register an open generic handler, register it manually through DI instead of relying on auto-registration.

```csharp
using Mocha;

public record OrderPlaced(Guid OrderId);

public class OrderPlacedHandler : IEventHandler<OrderPlaced>
{
    public ValueTask HandleAsync(
        OrderPlaced message,
        CancellationToken ct)
        => ValueTask.CompletedTask;
}
```

## MO0013

**Messaging handler is abstract**

|              |                                                        |
| ------------ | ------------------------------------------------------ |
| **Severity** | Warning                                                |
| **Message**  | `Handler '{0}' is abstract and will not be registered` |

### Cause

A class implements a messaging [handler](./handlers-and-consumers.md) interface but is declared `abstract`. The source generator skips abstract types because they cannot be instantiated.

### Example

```csharp
using Mocha;

public record OrderPlaced(Guid OrderId);

// Abstract handler - triggers MO0013
public abstract class OrderEventHandler : IEventHandler<OrderPlaced>
{
    public abstract ValueTask HandleAsync(
        OrderPlaced message,
        CancellationToken ct);
}
```

### Fix

Make the handler concrete. If you need shared base logic, move it to a base class that does not implement the handler interface.

```csharp
using Mocha;

public record OrderPlaced(Guid OrderId);

public class OrderPlacedHandler : IEventHandler<OrderPlaced>
{
    public ValueTask HandleAsync(
        OrderPlaced message,
        CancellationToken ct)
        => ValueTask.CompletedTask;
}
```

## MO0014

**Saga must have a public parameterless constructor**

|              |                                                           |
| ------------ | --------------------------------------------------------- |
| **Severity** | Error                                                     |
| **Message**  | `Saga '{0}' must have a public parameterless constructor` |

### Cause

A [`Saga<TState>`](./sagas.md) subclass does not have a public parameterless constructor. The saga infrastructure requires this constructor to instantiate the saga type. This is enforced by the `new()` constraint on the `AddSaga<T>` registration method.

### Example

```csharp
using Mocha.Sagas;

public class RefundSagaState : SagaStateBase
{
    public Guid OrderId { get; set; }
}

// Constructor requires a parameter - triggers MO0014
public class RefundSaga : Saga<RefundSagaState>
{
    private readonly ILogger _logger;

    public RefundSaga(ILogger logger)
    {
        _logger = logger;
    }
}
```

### Fix

Add a public parameterless constructor. Sagas are configured through their state machine definition, not through constructor injection. If you need dependencies, access them through the saga's built-in service resolution.

```csharp
using Mocha.Sagas;

public class RefundSagaState : SagaStateBase
{
    public Guid OrderId { get; set; }
}

public class RefundSaga : Saga<RefundSagaState>
{
    public RefundSaga()
    {
    }
}
```

## MO0015

**Missing JsonSerializerContext for AOT**

|              |                                                                                                                                                       |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Severity** | Error                                                                                                                                                 |
| **Message**  | `MessagingModule '{0}' must specify JsonContext when publishing for AOT. Add JsonContext = typeof(YourJsonContext) to the MessagingModule attribute.` |

### Cause

The project has `PublishAot` set to `true` but the `[assembly: MessagingModule]` attribute does not include a `JsonContext` property. AOT publishing requires a `JsonSerializerContext` so the source generator can produce trim-safe serialization code for all message types.

### Example

```csharp
using Mocha;

// No JsonContext specified while targeting AOT - triggers MO0015
[assembly: MessagingModule("OrderService")]
```

### Fix

Create a `JsonSerializerContext` that includes all your message types and reference it from the `MessagingModule` attribute.

```csharp
using System.Text.Json.Serialization;
using Mocha;

[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]

public record OrderPlaced(Guid OrderId);

[JsonSerializable(typeof(OrderPlaced))]
public partial class OrderServiceJsonContext : JsonSerializerContext;
```

## MO0016

**Missing JsonSerializable attribute**

|              |                                                                                                                                                |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **Severity** | Error                                                                                                                                          |
| **Message**  | `Type '{0}' is used as a message type but is not included in JsonSerializerContext '{1}'. Add [JsonSerializable(typeof({0}))] to the context.` |

### Cause

A type is used as a message, request, response, or saga state through a handler registration, but it is not declared via `[JsonSerializable(typeof(...))]` on the `JsonSerializerContext` specified in the `MessagingModule` attribute. Without this declaration, the AOT compiler cannot generate serialization code for the type.

### Example

```csharp
using System.Text.Json.Serialization;
using Mocha;

[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]

public record OrderPlaced(Guid OrderId);

public class OrderPlacedHandler : IEventHandler<OrderPlaced>
{
    public ValueTask HandleAsync(
        OrderPlaced message,
        CancellationToken ct)
        => ValueTask.CompletedTask;
}

// OrderPlaced is missing from the context - triggers MO0016
[JsonSerializable(typeof(string))]
public partial class OrderServiceJsonContext : JsonSerializerContext;
```

### Fix

Add a `[JsonSerializable]` attribute for every message type used by your handlers.

```csharp
using System.Text.Json.Serialization;
using Mocha;

[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]

public record OrderPlaced(Guid OrderId);

public class OrderPlacedHandler : IEventHandler<OrderPlaced>
{
    public ValueTask HandleAsync(
        OrderPlaced message,
        CancellationToken ct)
        => ValueTask.CompletedTask;
}

[JsonSerializable(typeof(OrderPlaced))]
public partial class OrderServiceJsonContext : JsonSerializerContext;
```

## MO0018

**Type not in JsonSerializerContext**

|              |                                                                                                                                            |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Severity** | Warning                                                                                                                                    |
| **Message**  | `Type '{0}' is used in a {1} call but is not included in JsonSerializerContext '{2}'. Add [JsonSerializable(typeof({0}))] to the context.` |

### Cause

Emitted only when `PublishAot` is enabled and a call site publishes a type not covered by the declared `JsonContext`. For example, `bus.PublishAsync<T>()` is called with a type that has no matching `[JsonSerializable(typeof(T))]` on the `JsonSerializerContext` referenced by `#!csharp [assembly: MessagingModule(..., JsonContext = typeof(...))]`. This is similar to [MO0016](#mo0016), but applies to types discovered at call sites rather than handler registrations. Without the declaration, the message cannot be serialized at runtime in an AOT environment.

This diagnostic does not fire when `PublishAot` is unset, even if `JsonContext` is declared on the module - opting into a source-generated context for non-AOT reasons does not require every call-site type to appear in it.

### Example

```csharp
using System.Text.Json.Serialization;
using Mocha;

[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]

public record OrderPlaced(Guid OrderId);
public record OrderShipped(Guid OrderId);

// OrderShipped is published but missing from the context - triggers MO0018
public class OrderService(IMessageBus bus)
{
    public async Task ShipOrderAsync(Guid orderId, CancellationToken ct)
    {
        await bus.PublishAsync(new OrderShipped(orderId), ct);
    }
}

[JsonSerializable(typeof(OrderPlaced))]
public partial class OrderServiceJsonContext : JsonSerializerContext;
```

### Fix

Add a `[JsonSerializable]` attribute for every type used at a call site.

```csharp
using System.Text.Json.Serialization;
using Mocha;

[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]

public record OrderPlaced(Guid OrderId);
public record OrderShipped(Guid OrderId);

[JsonSerializable(typeof(OrderPlaced))]
[JsonSerializable(typeof(OrderShipped))]
public partial class OrderServiceJsonContext : JsonSerializerContext;
```

# Mediator call-site diagnostics

These diagnostics are reported when the source generator inspects call sites that use `ISender` to dispatch commands or queries.

## MO0020

**Command/query sent but no handler found**

|              |                                                                                                         |
| ------------ | ------------------------------------------------------------------------------------------------------- |
| **Severity** | Warning                                                                                                 |
| **Message**  | `Type '{0}' is sent via {1} but no handler was found in this assembly. Ensure a handler is registered.` |

### Cause

A command or query type is dispatched via `ISender.SendAsync` or `ISender.QueryAsync`, but no corresponding handler implementation exists in the assembly. This catches cases where a call site references a message type that was never wired up with a handler.

### Example

```csharp
using Mocha.Mediator;

public record PlaceOrder(Guid OrderId, decimal Total) : ICommand;

// PlaceOrder is sent but no handler exists - triggers MO0020
public class OrderController(ISender sender)
{
    public async Task CreateOrderAsync(Guid orderId, CancellationToken ct)
    {
        await sender.SendAsync(new PlaceOrder(orderId, 99.99m), ct);
    }
}
```

### Fix

Implement a handler for the message type, or remove the call site if the handler is intentionally in another assembly.

```csharp
using Mocha.Mediator;

public record PlaceOrder(Guid OrderId, decimal Total) : ICommand;

public class PlaceOrderHandler : ICommandHandler<PlaceOrder>
{
    public ValueTask HandleAsync(
        PlaceOrder command,
        CancellationToken cancellationToken)
    {
        // process the order
        return ValueTask.CompletedTask;
    }
}
```

---

### Website Content Docs Mocha Exception Policies (`website/content/docs/mocha/exception-policies.md`)

---
title: "Exception Policies"
description: "Configure per-exception handling with composable retry, redelivery, and terminal actions."
---

Not every exception deserves the same treatment. A database deadlock might resolve on immediate retry. A downstream service outage needs minutes to recover. A validation error will never succeed no matter how many times you retry. Exception policies let you define per-exception handling strategies - retry, redeliver, dead-letter, or discard - as composable escalation chains in a single `AddResilience` call.

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        // Validation errors are permanent - route straight to the error endpoint
        policy.On<ValidationException>().DeadLetter();

        // Duplicate messages are safe to drop
        policy.On<DuplicateMessageException>().Discard();

        // Database deadlocks resolve quickly - retry then redeliver
        policy.On<NpgsqlException>(ex => ex.IsTransient)
            .Retry(5, TimeSpan.FromMilliseconds(200))
            .ThenRedeliver();

        // Everything else: retry 3 times, then redeliver on a schedule, then dead-letter
        policy.Default()
            .Retry()
            .ThenRedeliver()
            .ThenDeadLetter();
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

# How exception handling works

When a handler throws an exception, Mocha evaluates exception policies to determine what happens next. The decision flows through two pipeline stages - retry in the consumer pipeline and redelivery in the receive pipeline - before reaching the fault middleware as a last resort.

```mermaid
flowchart TD
    A[Handler throws exception] --> B{Exception policy\nmatches?}
    B -->|Match found| D{Terminal action?}
    B -->|No match| M[Route to error endpoint]

    D -->|Discard| E[Swallow exception\nMessage disappears]
    D -->|DeadLetter| F[Skip retry and redelivery\nRoute to error endpoint]
    D -->|No terminal| G{Retry configured?}

    G -->|Yes| H[Retry with configured settings]
    G -->|Disabled| K{Redelivery configured?}

    H -->|Success| I[Done]
    H -->|All retries exhausted| K

    K -->|Yes| L[Schedule redelivery\nMessage re-enters pipeline later]
    K -->|Disabled| M

    L -->|Success on redelivery| I
    L -->|All redeliveries exhausted| M
```

Each exception policy rule targets a specific exception type and defines an escalation chain. The chain controls which stages the message passes through and with what settings.

Exception matching respects inheritance. A policy on `NpgsqlException` also matches any subclass. When multiple rules could match, the most specific type wins - the same precedence as C# `catch` blocks.

# Configure exception policies

`AddResilience` is the single entry point for all exception handling configuration. There is no separate `AddRetry` or `AddRedelivery` call - retry and redelivery settings are configured per-exception within the policy.

> [!NOTE]
> Calling `On<T>()` for the same exception type replaces the previous rule for that type - last write wins. If you call `On<HttpRequestException>()` twice without a predicate, the second call overwrites the first. The same applies to `Default()`: calling it again replaces the previous default rule. For example, the parameterless `AddResilience()` registers `Default().Retry().ThenRedeliver()`. If you later call `#!csharp AddResilience(p => p.Default().Retry(5))`, the new default replaces the one registered by the parameterless overload.

## Parameterless defaults

The parameterless overload registers a catch-all `Default()` rule with both retry and redelivery enabled using built-in defaults:

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

This is equivalent to:

```csharp
.AddResilience(policy =>
{
    policy.Default().Retry().ThenRedeliver();
})
```

## Default() and On&lt;T&gt;()

`ExceptionPolicyOptions` exposes two methods for creating rules:

- **`Default()`** - shorthand for `On<Exception>()`. Configures the catch-all behavior for any exception that does not match a more specific rule.
- **`On<TException>()`** - configures behavior for a specific exception type.
- **`On<TException>(predicate)`** - configures behavior for a specific exception type when a predicate matches.

```csharp
.AddResilience(policy =>
{
    policy.On<NpgsqlException>().Retry(5).ThenRedeliver();
    policy.On<HttpRequestException>().Retry(3);
    policy.Default().Retry().ThenRedeliver();
})
```

## Bus-level policies

Bus-level policies apply to all endpoints and all consumers across the entire message bus.

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        policy.On<ValidationException>().DeadLetter();
        policy.On<DuplicateMessageException>().Discard();
        policy.Default().Retry().ThenRedeliver();
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

## Transport-level policies

Override bus-level policies for a specific transport. Transport-level policies replace the bus-level policies entirely for all endpoints on that transport - they are not merged.

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        policy.Default().Retry().ThenRedeliver();
    })
    .AddRabbitMQ(transport =>
    {
        transport.AddResilience(policy =>
        {
            policy.On<Exception>().DeadLetter();
        });
    });
```

## Consumer-level policies

Override policies for a specific consumer. Consumer-level policies replace the bus-level and transport-level policies for that consumer.

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        policy.On<Exception>()
            .Retry(2, TimeSpan.FromMilliseconds(100), RetryBackoffType.Constant);
    });

builder.Services.ConfigureMessageBus(bus =>
{
    bus.AddHandler<PaymentHandler>(consumer =>
    {
        consumer.AddResilience(policy =>
        {
            policy.On<Exception>()
                .Retry(5, TimeSpan.FromMilliseconds(500), RetryBackoffType.Exponential);
        });
    });
});
```

The `PaymentHandler` gets 5 retries with exponential backoff. All other consumers get the bus-level policy of 2 retries.

## Scope hierarchy

Exception policies resolve at four levels. The most specific scope wins, and replacement is atomic - the entire set of rules is replaced, not individual rules.

| Scope     | Applies to                            | Configured on                |
| --------- | ------------------------------------- | ---------------------------- |
| Bus       | All endpoints and consumers           | `IMessageBusBuilder`         |
| Host      | All message buses on the host         | `IMessageBusHostBuilder`     |
| Transport | All endpoints on a specific transport | `IReceiveMiddlewareProvider` |
| Consumer  | A single consumer                     | `IConsumerDescriptor`        |

```text
Consumer policies  →  Transport policies  →  Bus policies  →  Host policies
   (highest priority)                                        (lowest priority)
```

If a consumer defines exception policies, the bus-level and transport-level policies are ignored for that consumer. If you need a bus-level rule to also apply at the consumer level, include it in the consumer-level configuration.

# Terminal actions

Terminal actions end the message's lifecycle immediately. No retry, no redelivery - the message is either routed to the error endpoint or discarded.

## DeadLetter

`DeadLetter()` routes the message to the error endpoint, skipping both retry and redelivery. Use this for exceptions that are permanent - retrying will never succeed and you want the message preserved for inspection.

```csharp
policy.On<ValidationException>().DeadLetter();
policy.On<JsonException>().DeadLetter();
policy.On<UnauthorizedAccessException>().DeadLetter();
```

**When to use:** Malformed messages, authorization failures, schema violations, business rule violations that require manual intervention.

## Discard

`Discard()` swallows the exception and the message disappears. No error endpoint, no fault headers, no trace beyond logging.

```csharp
policy.On<DuplicateMessageException>().Discard();
policy.On<MessageExpiredException>().Discard();
```

**When to use:** Messages that are safe to lose - duplicates you have already processed, stale events that no longer matter. Use with caution: discarded messages leave no audit trail in the error endpoint.

# Retry policies

Retry re-runs the handler in-process using immediate retries. The message stays in memory, the concurrency slot is held, and the handler is invoked again after a short delay. Use retry for transient failures that resolve in milliseconds to seconds.

When you call `.Retry()` without chaining `.ThenRedeliver()`, redelivery is disabled for that exception type. If all retries are exhausted, the message routes to the error endpoint.

## Retry with defaults

```csharp
policy.On<TimeoutException>().Retry();
```

| Setting   | Default value |
| --------- | ------------- |
| Attempts  | 3             |
| Delay     | 200 ms        |
| Backoff   | Exponential   |
| Jitter    | Enabled       |
| Max delay | 30 seconds    |

## Retry with custom attempts

```csharp
policy.On<NpgsqlException>(ex => ex.IsTransient).Retry(5);
```

Overrides the number of retry attempts. Delay, backoff strategy, jitter, and max delay use the built-in defaults.

## Retry with full configuration

```csharp
policy.On<HttpRequestException>()
    .Retry(3, TimeSpan.FromMilliseconds(500), RetryBackoffType.Exponential);
```

Overrides attempts, base delay, and backoff strategy for this exception type.

## Retry with explicit intervals

```csharp
policy.On<SocketException>().Retry(
[
    TimeSpan.FromMilliseconds(100),
    TimeSpan.FromMilliseconds(500),
    TimeSpan.FromSeconds(2)
]);
```

Specifies the exact delay before each retry attempt. The array length determines the number of retries.

## Backoff strategies

| Strategy      | Behavior                            |
| ------------- | ----------------------------------- |
| `Constant`    | Same delay every attempt            |
| `Linear`      | `delay * attempt`                   |
| `Exponential` | `delay * 2^(attempt-1)` _(default)_ |

All strategies apply jitter by default to prevent thundering herd effects.

# Redelivery policies

Redelivery schedules the message for later delivery through the transport. The concurrency slot is released, the message re-enters the full receive pipeline on each redelivery attempt, and fresh retry cycles run on each delivery. Use redelivery for failures that need minutes or hours to resolve - a downstream service recovering from an outage, a rate limit resetting, or a database completing a failover.

When you call `.Redeliver()` directly (without `.Retry()` first), retry is disabled for that exception type. The handler failure goes straight to redelivery scheduling.

## Redeliver with defaults

```csharp
policy.On<ExternalServiceException>().Redeliver();
```

| Setting   | Default value         |
| --------- | --------------------- |
| Intervals | 5 min, 15 min, 30 min |
| Jitter    | Enabled               |
| Max delay | 1 hour                |

## Redeliver with custom attempts and delay

```csharp
policy.On<RateLimitException>().Redeliver(5, TimeSpan.FromMinutes(2));
```

Overrides the number of attempts and base delay.

## Redeliver with explicit intervals

```csharp
policy.On<ServiceUnavailableException>().Redeliver(
[
    TimeSpan.FromSeconds(30),
    TimeSpan.FromMinutes(5),
    TimeSpan.FromMinutes(30)
]);
```

Specifies the exact delay before each redelivery attempt. The array length determines the number of redelivery attempts.

# Escalation chains

The fluent API composes retry, redelivery, and terminal actions into escalation chains. The interface design enforces valid chains at compile time - you cannot chain `.ThenRedeliver()` after `.Redeliver()`, or `.Retry()` after `.ThenRedeliver()`.

## Retry then redeliver

```csharp
policy.On<NpgsqlException>(ex => ex.IsTransient)
    .Retry(3)
    .ThenRedeliver();
```

Try 3 immediate retries. If all fail, schedule redelivery with defaults (5, 15, 30 minutes). Each redelivery attempt runs a fresh cycle of 3 retries.

## Retry then redeliver with custom settings

```csharp
policy.On<PaymentGatewayException>()
    .Retry(5, TimeSpan.FromMilliseconds(500))
    .ThenRedeliver(
    [
        TimeSpan.FromMinutes(5),
        TimeSpan.FromMinutes(15),
        TimeSpan.FromMinutes(30)
    ])
    .ThenDeadLetter();
```

Try 5 immediate retries with 500 ms exponential backoff. If exhausted, schedule redeliveries at 5, 15, and 30 minutes. If all redeliveries are exhausted, route to the error endpoint.

## Retry then dead-letter

```csharp
policy.On<ConcurrencyException>()
    .Retry(3)
    .ThenDeadLetter();
```

Try 3 immediate retries. If all fail, skip redelivery and route to the error endpoint immediately.

## Chain behavior reference

| Chain                                        | Retry      | Redelivery | Terminal   |
| -------------------------------------------- | ---------- | ---------- | ---------- |
| `.Discard()`                                 | -          | -          | Discard    |
| `.DeadLetter()`                              | -          | -          | DeadLetter |
| `.Retry()`                                   | Enabled    | Disabled   | Default    |
| `.Retry(3)`                                  | 3 attempts | Disabled   | Default    |
| `.Redeliver()`                               | Disabled   | Enabled    | Default    |
| `.Retry(3).ThenRedeliver()`                  | 3 attempts | Enabled    | Default    |
| `.Retry(3).ThenDeadLetter()`                 | 3 attempts | Disabled   | DeadLetter |
| `.Retry(3).ThenRedeliver().ThenDeadLetter()` | 3 attempts | Enabled    | DeadLetter |

**Default terminal behavior:** When no terminal action is specified, exhausted messages route to the error endpoint through the fault middleware. `.ThenDeadLetter()` makes this intent explicit but does not change the behavior.

**Disabled vs. default:** "Disabled" means that tier is skipped for this exception type. A dash (-) means the tier is not configured and does not apply.

# Conditional policies

Use predicate overloads to apply policies only when an exception matches specific conditions. The predicate receives the typed exception instance.

## Filter by exception property

```csharp
// Only retry transient database errors
policy.On<NpgsqlException>(ex => ex.IsTransient)
    .Retry(5);

// Dead-letter non-transient database errors
policy.On<NpgsqlException>(ex => !ex.IsTransient)
    .DeadLetter();
```

## Filter by HTTP status code

```csharp
policy.On<HttpRequestException>(ex =>
    ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
    .Redeliver(
    [
        TimeSpan.FromSeconds(30),
        TimeSpan.FromMinutes(2),
        TimeSpan.FromMinutes(10)
    ]);

policy.On<HttpRequestException>(ex =>
    ex.StatusCode == System.Net.HttpStatusCode.BadRequest)
    .DeadLetter();
```

## Filter by inner exception

```csharp
policy.On<AggregateException>(ex =>
    ex.InnerException is TimeoutException)
    .Retry(3);
```

## Multiple rules for the same type

You can define multiple rules for the same exception type with different predicates. When an exception is thrown, the first matching rule wins.

```csharp
policy.On<HttpRequestException>(ex =>
    ex.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
    .Retry(3).ThenRedeliver();

policy.On<HttpRequestException>(ex =>
    ex.StatusCode == System.Net.HttpStatusCode.BadRequest)
    .DeadLetter();

// Catch-all for other HTTP errors
policy.On<HttpRequestException>().Retry(3);
```

---

### Website Content Docs Mocha Handler Registration (`website/content/docs/mocha/handler-registration.md`)

---
title: "Handler Registration"
description: "How Mocha discovers message bus handlers at compile time with a Roslyn source generator, and how to customize or mix in manual registration."
---

```csharp
builder.Services
    .AddMessageBus()
    .AddOrderService(); // source-generated from your assembly name
```

That registers the message bus, discovers your handlers and sagas at compile time, and wires up the registration. `.AddOrderService()` is a source-generated extension method - it knows your handler types at compile time and produces direct registration calls with no reflection.

# How the source generator works

At build time, the `Mocha.Analyzers` package runs a [Roslyn incremental source generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) that scans your assembly for classes implementing message bus handler interfaces. For each handler it finds, it emits a registration call in a generated extension method on `IMessageBusHostBuilder`.

The generator discovers these types:

| Interface                          | Pattern                |
| ---------------------------------- | ---------------------- |
| `IEventHandler<T>`                 | Pub/sub events         |
| `IEventRequestHandler<TReq, TRes>` | Request/reply          |
| `IEventRequestHandler<TReq>`       | Send (fire-and-forget) |
| `IBatchEventHandler<T>`            | Batch processing       |
| `IConsumer<T>`                     | Low-level consumer     |
| `Saga<TState>`                     | Saga orchestration     |

If you have used the [Mediator source generator](./mediator/index.md), this works the same way. The mediator generates `Add{ModuleName}()` on `IMediatorHostBuilder`; the message bus generates `Add{ModuleName}()` on `IMessageBusHostBuilder`.

> **Recommendation:** Always use the source generator for handler registration. The generated code uses optimized, reflection-free registration paths. The source-generated output is designed for long-term stability across versions. Manual registration methods are available for edge cases but their internal behavior may change between releases.

# Module naming

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

1. If you apply `#!csharp [assembly: MessagingModule("OrderService")]`, the method is `AddOrderService()`
2. Otherwise, it uses the last segment of the assembly name: `MyCompany.OrderService.Api` produces `AddApi()`

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

```csharp
using Mocha;

[assembly: MessagingModule("OrderService")]
```

This generates:

```csharp
builder.Services
    .AddMessageBus()
    .AddOrderService() // from [assembly: MessagingModule("OrderService")]
    .AddRabbitMQ();
```

> **Convention:** Use a short, meaningful name that identifies the service or bounded context - `OrderService`, `Billing`, `Inventory`. This name appears in the generated code and in your `Program.cs`, so keep it readable.

# What the generator produces

For a project named `OrderService` with an event handler, a request handler, and a saga, the source generator produces an extension method `AddOrderService()` on `IMessageBusHostBuilder`. This method registers all discovered handlers and sagas with optimized, reflection-free factory delegates.

Handlers are grouped by kind and ordered alphabetically within each group. The registration order is: batch handlers, consumers, request handlers, event handlers, sagas.

> [!NOTE]
> The generated code is an implementation detail and may change between versions. Do not depend on the shape of the generated output.

# Manual handler registration

When you need to register handlers outside the source generator's reach - from a plugin assembly, a dynamically loaded module, or in integration tests - use the explicit registration methods:

```csharp
builder.Services
    .AddMessageBus()
    .AddOrderService()                                    // source-generated handlers
    .AddEventHandler<ExternalNotificationHandler>()       // from another assembly
    .AddRequestHandler<PluginPaymentHandler>()            // from a plugin
    .AddRabbitMQ();
```

You can mix source-generated and manual registration freely. If both the source generator and manual code register the same handler type, the configurations are composed - the source generator sets up the base registration and your manual call layers additional configuration (such as consumer middleware) on top.

> **Prefer the source generator.** Manual registration methods use runtime reflection to create handler consumers. The source generator produces direct, reflection-free factory calls. We guarantee backwards compatibility for the source-generated registration path; the manual registration API is stable at the surface level but its internal behavior may evolve.

# Troubleshooting

## The source-generated method does not appear

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

- Confirm the `Mocha.Analyzers` package is referenced with `OutputItemType="Analyzer"` in your `.csproj`
- Rebuild the project - source generators run during compilation
- Check the build output for [analyzer diagnostics](./diagnostics.md) prefixed with `MO`
- Verify you have at least one concrete handler class in the assembly

## Handler is not being called

If the source-generated method is available but a specific handler does not run:

- Check for [**MO0013**](./diagnostics.md#mo0013) (abstract handler) - only concrete classes are registered
- Check for [**MO0012**](./diagnostics.md#mo0012) (open generic) - close the generic type
- Verify the handler implements the correct interface for the messaging pattern you are using
- Ensure the handler is in the same project that references `Mocha.Analyzers`

## Priority when a handler implements multiple interfaces

If a class implements more than one messaging interface (e.g., both `IBatchEventHandler<T>` and `IEventHandler<T>`), the source generator registers it using the highest-priority interface only:

`IBatchEventHandler` > `IConsumer` > `IEventRequestHandler<T,R>` > `IEventRequestHandler<T>` > `IEventHandler`

# Next steps

- [Handlers and Consumers](./handlers-and-consumers.md) - handler interfaces, DI scoping, and exception behavior
- [Routing and Endpoints](./routing-and-endpoints.md) - how the bus routes messages to registered handlers
- [Sagas](./sagas.md) - saga state machines and long-running workflows
- [Mediator](./mediator/index.md) - the mediator uses the same source generation approach for in-process CQRS

---

### Website Content Docs Mocha Handlers And Consumers (`website/content/docs/mocha/handlers-and-consumers.md`)

---
title: "Handlers and Consumers"
description: "Learn how to implement Mocha message handlers, from event, request, send, and batch handlers down to the low-level consumer interface."
---

You implement a handler interface, and the source generator discovers it at compile time. Mocha routes matching messages to your handler automatically. This page covers every handler type, when to use each one, and the patterns that apply to all of them: DI scoping, exception behavior, and publishing from within a handler.

## When to use which handler

Choose a handler interface based on the messaging pattern you are implementing:

| Interface                          | Use when                                                                                        |
| ---------------------------------- | ----------------------------------------------------------------------------------------------- |
| `IEventHandler<T>`                 | Reacting to a published event. No reply expected. Multiple handlers can receive the same event. |
| `IEventRequestHandler<TReq, TRes>` | Handling a request and returning a typed response to the caller.                                |
| `IEventRequestHandler<TReq>`       | Handling a send (fire-and-forget) with no typed response.                                       |
| `IBatchEventHandler<T>`            | Processing multiple events at once for throughput efficiency.                                   |
| `IConsumer<T>`                     | Accessing raw envelope metadata - headers, correlation IDs, or the full consume context.        |

If you have read [Messaging Patterns](./messaging-patterns.md), these map directly: `IEventHandler<T>` is for `PublishAsync`, `IEventRequestHandler<TReq>` is for `SendAsync`, and `IEventRequestHandler<TReq, TRes>` is for `RequestAsync`.

# Event handler

An event handler reacts to published events. The publisher does not know or care who handles the event. Multiple handlers can receive the same event type independently.

By the end of this section, you will have a working event handler that logs published orders.

## Define the message

```csharp filename="OrderPlaced.cs"
namespace MyApp.Messages;

public sealed record OrderPlaced
{
    public required Guid OrderId { get; init; }
    public required string CustomerId { get; init; }
    public required decimal TotalAmount { get; init; }
}
```

Messages are plain C# records. No base class or marker interface required for pub/sub events.

## Implement the handler

```csharp filename="OrderPlacedHandler.cs"
using Mocha;
using MyApp.Messages;

namespace MyApp.Handlers;

public class OrderPlacedHandler(ILogger<OrderPlacedHandler> logger)
    : IEventHandler<OrderPlaced>
{
    public async ValueTask HandleAsync(
        OrderPlaced message,
        CancellationToken cancellationToken)
    {
        logger.LogInformation(
            "Order placed: {OrderId} for customer {CustomerId}, total {TotalAmount}",
            message.OrderId,
            message.CustomerId,
            message.TotalAmount);

        await Task.CompletedTask;
    }
}
```

`IEventHandler<T>` has a single method: `HandleAsync(T message, CancellationToken cancellationToken)`. The bus deserializes the message and calls your handler. Constructor dependencies are resolved from a scoped DI container - more on this in [DI scoping](#di-scoping).

## Register and run

```csharp filename="Program.cs"
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddMessageBus()
    .AddMyApp() // source-generated - registers OrderPlacedHandler automatically
    .AddInMemory(); // or .AddRabbitMQ()

var app = builder.Build();
app.Run();
```

`.AddMyApp()` is a source-generated extension method that discovers all handlers in the assembly and registers them. The source generator found `OrderPlacedHandler`, saw that it implements `IEventHandler<OrderPlaced>`, and emitted a registration call for it. For details on how the source generator works and how to customize the module name, see [Handler Registration](./handler-registration.md).

## Verify the handler runs

Publish an event from an API endpoint. `IMessageBus` is a scoped service - resolve it through endpoint injection:

```csharp
app.MapPost("/orders", async (IMessageBus bus) =>
{
    await bus.PublishAsync(new OrderPlaced
    {
        OrderId = Guid.NewGuid(),
        CustomerId = "customer-42",
        TotalAmount = 149.99m
    }, CancellationToken.None);

    return Results.Ok("Published");
});
```

If everything worked, you see this in the console:

```text
info: MyApp.Handlers.OrderPlacedHandler[0]
      Order placed: 3f2504e0-4f89-11d3-9a0c-0305e82c3301 for customer customer-42, total 149.99
```

# Request handler

A request handler processes a request and returns a typed response. The caller awaits the result. Use this when the sender needs data back.

## Define the request and response

The request record must implement `IEventRequest<TResponse>`. This marker interface tells Mocha the expected response type and enables type-safe `RequestAsync` calls.

```csharp filename="GetProductRequest.cs"
using Mocha;

namespace MyApp.Messages;

public sealed record GetProductRequest : IEventRequest<GetProductResponse>
{
    public required Guid ProductId { get; init; }
}

public sealed record GetProductResponse
{
    public required Guid ProductId { get; init; }
    public required string Name { get; init; }
    public required decimal Price { get; init; }
    public required bool IsAvailable { get; init; }
}
```

## Implement the handler

```csharp filename="GetProductRequestHandler.cs"
using Mocha;
using MyApp.Messages;

namespace MyApp.Handlers;

public class GetProductRequestHandler(AppDbContext db)
    : IEventRequestHandler<GetProductRequest, GetProductResponse>
{
    public async ValueTask<GetProductResponse> HandleAsync(
        GetProductRequest request,
        CancellationToken cancellationToken)
    {
        var product = await db.Products
            .FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken);

        return new GetProductResponse
        {
            ProductId = request.ProductId,
            Name = product?.Name ?? string.Empty,
            Price = product?.Price ?? 0,
            IsAvailable = product?.StockQuantity > 0
        };
    }
}
```

The return value is sent back to the caller automatically. The return value must not be `null` - if you return `null`, the bus throws an `InvalidOperationException`.

## Register and call

```csharp
builder.Services
    .AddMessageBus()
    .AddMyApp() // source-generated - registers GetProductRequestHandler automatically
    .AddRabbitMQ();
```

From the caller side:

```csharp
var response = await bus.RequestAsync(
    new GetProductRequest { ProductId = productId },
    cancellationToken);

// response.Name, response.Price, response.IsAvailable
```

If the handler throws, the exception propagates back to the caller. See [Exception behavior](#exception-behavior) for details.

# Send handler

A send handler processes a one-way instruction. There is no typed response. The sender dispatches the message and moves on.

## Define the message

Send messages do not implement `IEventRequest<T>` because there is no typed response.

```csharp filename="ReserveInventoryCommand.cs"
namespace MyApp.Messages;

public sealed record ReserveInventoryCommand
{
    public required Guid OrderId { get; init; }
    public required Guid ProductId { get; init; }
    public required int Quantity { get; init; }
}
```

## Implement the handler

Use `IEventRequestHandler<TRequest>` - the single type parameter variant, with no response type:

```csharp filename="ReserveInventoryCommandHandler.cs"
using Mocha;
using MyApp.Messages;

namespace MyApp.Handlers;

public class ReserveInventoryCommandHandler(
    AppDbContext db,
    ILogger<ReserveInventoryCommandHandler> logger)
    : IEventRequestHandler<ReserveInventoryCommand>
{
    public async ValueTask HandleAsync(
        ReserveInventoryCommand request,
        CancellationToken cancellationToken)
    {
        logger.LogInformation(
            "Reserving {Quantity} units of product {ProductId}",
            request.Quantity,
            request.ProductId);

        var product = await db.Products
            .FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken);

        if (product is null)
        {
            throw new InvalidOperationException(
                $"Product {request.ProductId} not found");
        }

        product.StockQuantity -= request.Quantity;
        await db.SaveChangesAsync(cancellationToken);
    }
}
```

## Register and send

```csharp
builder.Services
    .AddMessageBus()
    .AddMyApp() // source-generated - registers ReserveInventoryCommandHandler automatically
    .AddRabbitMQ();
```

Send the message:

```csharp
await bus.SendAsync(new ReserveInventoryCommand
{
    OrderId = orderId,
    ProductId = productId,
    Quantity = 3
}, cancellationToken);
```

Expected output:

```text
info: MyApp.Handlers.ReserveInventoryCommandHandler[0]
      Reserving 3 units of product a1b2c3d4-...
```

`SendAsync` completes after the message is dispatched to the transport. It does not wait for the handler to finish. To wait for completion, use `RequestAsync` instead - Mocha sends an automatic acknowledgment when the handler finishes.

# Batch handler

A batch handler receives groups of messages at once instead of one at a time. Use batch handlers for high-throughput scenarios where processing messages in bulk is more efficient - bulk database writes, aggregations, or analytics pipelines.

## Implement the handler

```csharp filename="OrderPlacedBatchHandler.cs"
using Mocha;
using MyApp.Messages;

namespace MyApp.Handlers;

public class OrderPlacedBatchHandler(
    AppDbContext db,
    ILogger<OrderPlacedBatchHandler> logger)
    : IBatchEventHandler<OrderPlaced>
{
    public async ValueTask HandleAsync(
        IMessageBatch<OrderPlaced> batch,
        CancellationToken cancellationToken)
    {
        logger.LogInformation(
            "Processing batch of {Count} orders",
            batch.Count);

        var totalRevenue = 0m;
        foreach (var order in batch)
        {
            totalRevenue += order.TotalAmount;
        }

        db.RevenueSummaries.Add(new RevenueSummary
        {
            OrderCount = batch.Count,
            TotalRevenue = totalRevenue,
            CreatedAt = DateTimeOffset.UtcNow
        });

        await db.SaveChangesAsync(cancellationToken);

        logger.LogInformation(
            "Revenue summary created: {Count} orders, {Total:C} total",
            batch.Count,
            totalRevenue);
    }
}
```

`IMessageBatch<T>` implements `IReadOnlyList<T>`, so you can iterate, index, and check `.Count`. The `CompletionMode` property tells you why the batch was dispatched: `Size` (reached the configured maximum), `Time` (timeout expired), or `Forced` (endpoint shutting down).

To access envelope metadata for a specific message in the batch, call `batch.GetContext(index)` to get an `IConsumeContext<T>` with headers, correlation IDs, and timestamps.

## Register and configure

```csharp
builder.Services
    .AddMessageBus()
    .AddMyApp() // source-generated - registers OrderPlacedBatchHandler automatically
    .AddBatchHandler<OrderPlacedBatchHandler>(opts =>
    {
        opts.MaxBatchSize = 50;
        opts.BatchTimeout = TimeSpan.FromSeconds(10);
    })
    .AddRabbitMQ();
```

The source generator registers the batch handler, but you can chain `#!csharp .AddBatchHandler<T>(opts => ...)` after `AddMyApp()` to override batch configuration options. Without explicit configuration, Mocha uses the defaults: 100 messages per batch, 1-second timeout.

## Publish events as normal

```csharp
for (var i = 0; i < 100; i++)
{
    await bus.PublishAsync(new OrderPlaced
    {
        OrderId = Guid.NewGuid(),
        CustomerId = $"customer-{i}",
        TotalAmount = 99.99m
    }, cancellationToken);
}
```

With `MaxBatchSize = 50`, the 100 messages arrive as two batches of 50. Expected output:

```text
info: MyApp.Handlers.OrderPlacedBatchHandler[0]
      Processing batch of 50 orders
info: MyApp.Handlers.OrderPlacedBatchHandler[0]
      Revenue summary created: 50 orders, $4,999.50 total
info: MyApp.Handlers.OrderPlacedBatchHandler[0]
      Processing batch of 50 orders
info: MyApp.Handlers.OrderPlacedBatchHandler[0]
      Revenue summary created: 50 orders, $4,999.50 total
```

# Advanced: accessing the envelope

For cases where you need the full consume context - message headers, correlation IDs, source addresses - implement `IConsumer<T>` instead of a handler interface.

```csharp filename="OrderAuditConsumer.cs"
using Mocha;
using MyApp.Messages;

namespace MyApp.Consumers;

public class OrderAuditConsumer(ILogger<OrderAuditConsumer> logger)
    : IConsumer<OrderPlaced>
{
    public async ValueTask ConsumeAsync(
        IConsumeContext<OrderPlaced> context,
        CancellationToken cancellationToken)
    {
        var order = context.Message;

        logger.LogInformation(
            "Audit: OrderId={OrderId} MessageId={MessageId} " +
            "CorrelationId={CorrelationId} Source={Source}",
            order.OrderId,
            context.MessageId,
            context.CorrelationId,
            context.SourceAddress);

        // Read custom headers attached at publish time
        if (context.Headers.TryGetValue("x-tenant", out var tenant))
        {
            logger.LogInformation("Tenant: {Tenant}", tenant);
        }

        await Task.CompletedTask;
    }
}
```

`IConsumeContext<T>` gives you the deserialized message plus envelope fields: `MessageId`, `CorrelationId`, `ConversationId`, `CausationId`, `SourceAddress`, `DestinationAddress`, `SentAt`, `Headers`, `DeliveryCount`, and more. See [Messages](./messages.md) for how correlation identifiers relate to each other.

Register with `.AddConsumer<T>()`:

```csharp
builder.Services
    .AddMessageBus()
    .AddMyApp() // source-generated - registers OrderAuditConsumer automatically
    .AddRabbitMQ();
```

Use `IConsumer<T>` when you need envelope metadata or custom header inspection. For business logic that operates on the message payload alone, handler interfaces are simpler.

# DI scoping

Mocha creates a new DI scope for each message. Your handler is instantiated from that scope, its constructor dependencies are resolved from it, and the scope is disposed when the handler completes.

This means `DbContext` and other scoped services are safe to inject directly into handler constructors:

```csharp
// AppDbContext is a scoped service - safe to inject
public class OrderPlacedHandler(AppDbContext db, ILogger<OrderPlacedHandler> logger)
    : IEventHandler<OrderPlaced>
{
    public async ValueTask HandleAsync(
        OrderPlaced message,
        CancellationToken cancellationToken)
    {
        db.ProcessedOrders.Add(new ProcessedOrder { OrderId = message.OrderId });
        await db.SaveChangesAsync(cancellationToken);
    }
}
```

Each message gets its own scope and its own `DbContext` instance. Two messages processing concurrently do not share a `DbContext`.

Singleton services are resolved from the root container as usual. If you inject a singleton that holds scoped state, you will get unexpected behavior - the same problem as in any ASP.NET Core application.

# Exception behavior

When `HandleAsync` throws, the behavior depends on the handler type and the middleware pipeline:

- **Event handlers and send handlers:** The exception is caught by the pipeline. By default, Mocha retries the message according to the configured retry policy, then moves it to the dead-letter queue if retries are exhausted. See [Reliability](./reliability.md) for retry and fault configuration.
- **Request handlers:** The exception propagates back to the caller as a fault. If you use `RequestAsync`, it throws on the caller side. The caller receives the error, not a timeout.
- **Batch handlers:** If the handler throws, all messages in the batch fault together. The pipeline treats the entire batch as a failed unit.

When a message arrives, it passes through middleware before reaching your handler. The pipeline handles fault routing, dead-letter delivery, observability, and concurrency limits - without any code in your handler. See [Middleware and Pipelines](./middleware-and-pipelines.md) for details on writing custom pipeline middleware.

# Publishing from a handler

To publish a message from within a handler, inject `IMessageBus` via the constructor:

```csharp
public class OrderPlacedHandler(
    AppDbContext db,
    IMessageBus messageBus,
    ILogger<OrderPlacedHandler> logger)
    : IEventHandler<OrderPlaced>
{
    public async ValueTask HandleAsync(
        OrderPlaced message,
        CancellationToken cancellationToken)
    {
        // Handle the inbound event
        var invoice = new Invoice { OrderId = message.OrderId };
        db.Invoices.Add(invoice);
        await db.SaveChangesAsync(cancellationToken);

        // Publish a downstream event
        await messageBus.PublishAsync(
            new InvoiceCreated
            {
                InvoiceId = invoice.Id,
                OrderId = message.OrderId,
                Amount = message.TotalAmount
            },
            cancellationToken);

        logger.LogInformation(
            "Invoice created and InvoiceCreated published for order {OrderId}",
            message.OrderId);
    }
}
```

Messages published from within a handler automatically inherit the `ConversationId` and `CorrelationId` from the inbound message. The bus sets `CausationId` on the outgoing message to the `MessageId` of the inbound message. This creates a traceable parent-child chain across services without any extra code. See [Messages](./messages.md) for how correlation identifiers work.

# Further reading

- [Handler Registration](./handler-registration.md) - How the source generator discovers handlers and how to customize registration.
- [Event-Driven Consumer](https://www.enterpriseintegrationpatterns.com/patterns/messaging/EventDrivenConsumer.html) - The EIP pattern that defines push-based message consumption, which is what Mocha's handlers implement.
- [Competing Consumers](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CompetingConsumers.html) - When multiple instances of your service run, they compete for messages on the same queue. This is the concurrency model for Mocha handlers under load.

# Next steps

Your handlers are registered. Learn how the source generator discovers and registers them in [Handler Registration](./handler-registration.md), or how Mocha routes messages to them in [Routing and Endpoints](./routing-and-endpoints.md).

> **Runnable examples:** [BatchHandler](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/HandlersAndConsumers/BatchHandler), [LowLevelConsumer](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/HandlersAndConsumers/LowLevelConsumer), [CustomConsumer](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/HandlersAndConsumers/CustomConsumer)
>
> **Full demo:** [Demo.Billing](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Billing) shows event handlers (`OrderPlacedEventHandler`), batch handlers (`OrderPlacedBatchHandler` with revenue aggregation, `BulkOrderBatchHandler` for high-volume processing), and request handlers (`ProcessRefundCommandHandler`).

---

### Website Content Docs Mocha Hosting (`website/content/docs/mocha/hosting.md`)

---
title: "Hosting"
description: "Integrate Mocha with ASP.NET Core using the Mocha.Hosting package for health checks."
---

The `Mocha.Hosting` package provides ASP.NET Core integrations for the message bus: health checks that verify end-to-end connectivity through serialization, transport, routing, and handler execution.

```shell
dotnet add package Mocha.Hosting
```

# Health checks

Mocha integrates with the [ASP.NET Core health checks](https://learn.microsoft.com/aspnet/core/host-and-monitor/health-checks) system. The health check sends a `HealthRequest` message through the bus and waits for a `HealthResponse`. This verifies the full pipeline - serialization, transport, routing, and handler execution - not just that the broker is reachable.

## Register the health check handler

On the bus builder, call `.AddHealthCheck()` to register the built-in `HealthRequestHandler`. This handler responds to `HealthRequest` messages with an `"OK"` response.

```csharp
using Mocha;
using Mocha.Hosting;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddMessageBus()
    .AddHealthCheck() // Registers the HealthRequestHandler
    .AddMyApp()       // source-generated handler registration
    .AddRabbitMQ();
```

## Add the health check to ASP.NET Core

Use the `AddMessageBus()` extension on `IHealthChecksBuilder` to register a health check that sends a request through the bus and verifies the response:

```csharp
builder.Services
    .AddHealthChecks()
    .AddMessageBus(); // Sends HealthRequest via RequestAsync and checks the reply
```

Then map the health check endpoint:

```csharp
var app = builder.Build();

app.MapHealthChecks("/health");

app.Run();
```

A `GET /health` request will now include the message bus status. If the bus cannot process and reply to the health request within the timeout, the check reports `Unhealthy`.

## Target a specific endpoint

By default the health check uses the bus's default routing to deliver the `HealthRequest`. To target a specific endpoint (useful when you have multiple transports or want to verify a particular service), pass a URI:

```csharp
builder.Services
    .AddHealthChecks()
    .AddMessageBus(new Uri("queue://my-service-health"));
```

The health check is registered with the `"ready"` and `"live"` tags, so you can use tag-based filtering to separate readiness from liveness probes:

```csharp
app.MapHealthChecks("/health/ready", new()
{
    Predicate = check => check.Tags.Contains("ready")
});

app.MapHealthChecks("/health/live", new()
{
    Predicate = check => check.Tags.Contains("live")
});
```

# Next steps

- [Observability](./observability.md) - Add OpenTelemetry tracing and metrics to the bus.
- [Reliability](./reliability.md) - Configure outbox, inbox, and circuit breakers.
- [Transports](./transports/index.md) - Configure RabbitMQ, InMemory, and multi-transport setups.

---

### Website Content Docs Mocha Messages (`website/content/docs/mocha/messages.md`)

---
title: "Messages"
description: "Understand the message envelope, naming conventions, correlation, and message type identity in Mocha, and learn how to define message types."
---

A message is any C# class, record, or struct. No base class, no marker interface, no framework attributes. You define a type that holds your business data, and Mocha handles everything else - routing, serialization, correlation, and delivery. Records are used throughout the examples because they naturally fit message semantics, but they are not required. Mocha serializes message bodies as JSON by default.

# Why envelopes exist

When a message crosses a process boundary, it carries more than your business payload. The receiving service needs to know what type the message is, where it came from, when it was sent, and how it relates to other messages in a workflow. None of this belongs in your domain model.

Mocha solves this with the **envelope pattern**: every message is wrapped in an envelope containing headers and the serialized body. Your POCO contains business data; the envelope contains infrastructure metadata. Keep them separate.

This separation means your `OrderPlaced` record stays a clean contract with business properties. The envelope wraps it with everything the infrastructure needs: correlation identifiers, addressing, timestamps, and custom headers. The bus builds the envelope automatically when you publish or send - most of the time you never interact with it directly.

# Naming conventions

> **Convention:** Name commands in imperative verb-noun form (`PlaceOrder`, `ProcessPayment`). Name events in past-tense noun-verb form (`OrderPlaced`, `PaymentProcessed`). The name communicates intent - commands request action, events announce what happened.

Following this convention makes the intent of each message type clear at a glance and keeps your codebase consistent with the broader .NET messaging ecosystem.

# Define and use messages

This section walks through defining a message, attaching custom headers when publishing, and reading envelope metadata inside a handler.

## Define a message

Messages can be any class, record, or struct. Records with `{ get; init; }` properties are a natural fit:

```csharp filename="OrderPlaced.cs"
namespace MyApp;

public sealed record OrderPlaced
{
    public required Guid OrderId { get; init; }
    public required string CustomerId { get; init; }
    public required decimal TotalAmount { get; init; }
}
```

The record holds business data only. No framework types, no base classes, no marker interfaces.

## Publish with custom headers

To attach custom metadata, pass `PublishOptions` with a `Headers` dictionary:

```csharp
await bus.PublishAsync(
    new OrderPlaced
    {
        OrderId = Guid.NewGuid(),
        CustomerId = "CUST-001",
        TotalAmount = 99.95m
    },
    new PublishOptions
    {
        Headers = new() { ["x-tenant"] = "acme", ["x-trace-id"] = "abc-123" }
    },
    CancellationToken.None);
```

For commands, use `SendOptions` with the same `Headers` property:

```csharp
await bus.SendAsync(
    new ProcessPayment { OrderId = "ORD-1", Amount = 99.95m },
    new SendOptions
    {
        Headers = new() { ["x-tenant"] = "acme" }
    },
    CancellationToken.None);
```

The bus merges your headers into the envelope's header collection before dispatching.

## Message headers

A header value must map to one of these portable representations: `null`, a boolean, a string, a
number, an object with string keys, or an array. You can set headers with the following CLR values;
Mocha converts them as shown:

| Value supplied                                                         | Header value                                         |
| ---------------------------------------------------------------------- | ---------------------------------------------------- |
| `null`, `bool`, `string`                                               | `null`, boolean, or string                           |
| `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`     | integer number                                       |
| `float`, `double`, `decimal`                                           | number                                               |
| `char`                                                                 | one-character string                                 |
| `Guid`                                                                 | GUID string                                          |
| `Uri`                                                                  | original URI string                                  |
| an enum                                                                | member name                                          |
| `DateTime`, `DateTimeOffset`, `DateOnly`, `TimeOnly`                   | ISO 8601 string                                      |
| `TimeSpan`                                                             | constant-format (`c`) string                         |
| `byte[]`, `ArraySegment<byte>`, `ReadOnlyMemory<byte>`, `Memory<byte>` | base64 string                                        |
| a JSON scalar                                                          | the corresponding `null`, boolean, string, or number |
| a JSON object, nested headers, or a dictionary with string keys        | object whose values are mapped recursively           |
| a JSON array or sequence                                               | array whose values are mapped recursively            |

Dictionary keys must be strings, and every value in a dictionary or sequence must itself be a
supported header value. Use a string when the exact text representation matters, such as for a
high-precision decimal or an application-specific identifier. Other CLR types are not supported as
header values.

## Access envelope metadata in a handler

`IEventHandler<T>` receives the deserialized message and a cancellation token - that is all. To read message IDs, correlation IDs, timestamps, or custom headers, implement `IConsumer<T>` instead. The `IConsumeContext<T>` parameter gives you both the deserialized message and all envelope fields:

```csharp filename="OrderAuditConsumer.cs"
using Mocha;

namespace MyApp;

public class OrderAuditConsumer(ILogger<OrderAuditConsumer> logger)
    : IConsumer<OrderPlaced>
{
    public ValueTask ConsumeAsync(
        IConsumeContext<OrderPlaced> context,
        CancellationToken cancellationToken)
    {
        // The deserialized message
        var order = context.Message;

        // Envelope metadata - generated by the bus automatically
        logger.LogInformation(
            "MessageId={MessageId} CorrelationId={CorrelationId} " +
            "ConversationId={ConversationId} SentAt={SentAt}",
            context.MessageId,
            context.CorrelationId,
            context.ConversationId,
            context.SentAt);

        // Custom headers you attached when publishing
        if (context.Headers.TryGetValue("x-tenant", out var tenant))
        {
            logger.LogInformation("Tenant: {Tenant}", tenant);
        }

        return default;
    }
}
```

Register a consumer with `.AddConsumer<T>()`:

```csharp
builder.Services
    .AddMessageBus()
    .AddConsumer<OrderAuditConsumer>()
    .AddInMemory();
```

When the handler processes a published `OrderPlaced` event, you see output like:

```text
MessageId=3f2a... CorrelationId=7b1c... ConversationId=9d4e... SentAt=2026-02-25T10:30:00Z
Tenant: acme
```

The bus generates `MessageId`, `CorrelationId`, and `ConversationId` automatically. Your custom headers appear alongside them.

> [!TIP]
> Use `IEventHandler<T>` when you only need the message payload. Switch to `IConsumer<T>` when you need envelope metadata or custom headers. Both can coexist for the same message type - Mocha routes to all registered handlers and consumers.

# How correlation works

Mocha uses three identifiers to track relationships between messages:

```text
ConversationId ─── groups all messages in a logical conversation
  │
  ├── CorrelationId ─── chains messages in a specific workflow
  │     │
  │     ├── CausationId ─── links parent → child
  │     │     │
  │     │     └── CausationId ─── links parent → child
  │     │
  │     └── CausationId ─── links parent → child
  │
  └── CorrelationId ─── a different workflow in the same conversation
```

**ConversationId** is the broadest scope. When you publish the first message in a flow, the bus generates a `ConversationId`. Every subsequent message in that conversation - across services, across handler chains - inherits the same `ConversationId`. Use it to find all messages that belong to a single business transaction.

**CorrelationId** is narrower. It groups messages within a specific workflow or saga instance. A single conversation may contain multiple correlation scopes - for example, an order saga and a payment saga both triggered by the same initial event.

**CausationId** traces direct causality. When a handler publishes or sends a new message in response to a received message, the bus sets the new message's `CausationId` to the `MessageId` of the received message. This creates a parent-child chain you can follow to reconstruct the exact sequence of events.

Together, these three identifiers give you full traceability without adding any fields to your message records.

# How message type resolution works

When you register a message type - explicitly with `AddMessage<T>()` or implicitly by adding a handler - Mocha assigns it a URN-based identity:

```text
urn:message:<namespace>:<type-name>
```

For example, `MyApp.Contracts.OrderPlaced` becomes:

```text
urn:message:my-app.contracts:order-placed
```

The bus stores this identity in the `MessageType` field of the envelope. On the receiving side, the message type selection middleware matches the incoming URN against the registered types to find the correct CLR type for deserialization.

Use `AddMessage<T>()` to configure a message type explicitly - for example, to pin its URN when refactoring CLR namespaces, or to configure a send route:

```csharp
builder.Services
    .AddMessageBus()
    .AddMessage<OrderPlaced>(d =>
    {
        d.Send(route => route.ToQueue("orders-queue"));
    })
    .AddInMemory();
```

**Polymorphic messages.** If a message class implements interfaces or extends base classes that are also registered as message types, the envelope carries all of those identities in the `EnclosedMessageTypes` array. This allows a handler registered for an interface to receive messages that implement it.

# Message versioning

Evolving message contracts requires care because producers and consumers may deploy independently. Adding a new `init` property with a default value is backward-compatible - existing consumers that do not know about the property ignore it during deserialization. Renaming or removing a required property is breaking and requires a coordinated deployment or a versioning strategy.

When you need to refactor a message type's CLR namespace without changing its wire identity, use `AddMessage<T>()` to pin the URN explicitly. This decouples the type's wire identity from its CLR location, so consumers continue to receive the message under the old URN even after you move or rename the class.

# Next steps

Now that you understand message structure, learn the three messaging patterns.

- [**Messaging Patterns**](./messaging-patterns.md) - Pub/sub events, point-to-point commands, and request/reply.

> **Full demo:** [Demo.Contracts](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Contracts) contains a complete set of message contracts for an e-commerce system - events (`OrderPlacedEvent`, `PaymentCompletedEvent`), send messages (`ProcessRefundCommand`, `ReserveInventoryCommand`), and request/reply pairs used by sagas.

---

### Website Content Docs Mocha Messaging Patterns (`website/content/docs/mocha/messaging-patterns.md`)

---
title: "Messaging Patterns"
description: "Learn the three core messaging patterns in Mocha - events (pub/sub), send (fire-and-forget), and request/reply - and when to use each one."
---

Every message you send answers one of three questions: "Who needs to know?" - an event, broadcast to anyone who cares. "Who should act?" - a command, directed at a single handler. "What is the result?" - a request that blocks until the handler replies. Choosing the wrong pattern is the most common messaging architecture mistake. This page explains each pattern, when to use it, and the anti-pattern to avoid.

| Pattern             | Bus method     | Handler interface                           | Delivery                                    |
| ------------------- | -------------- | ------------------------------------------- | ------------------------------------------- |
| **Event** (pub/sub) | `PublishAsync` | `IEventHandler<TEvent>`                     | One-to-many: all subscribers receive a copy |
| **Request** (send)  | `SendAsync`    | `IEventRequestHandler<TRequest>`            | One-to-one: a single handler processes it   |
| **Request/Reply**   | `RequestAsync` | `IEventRequestHandler<TRequest, TResponse>` | One-to-one: sender awaits a typed response  |

# Events (pub/sub)

Events represent something that happened. The publisher does not know or care who receives the event - that question is answered by whoever subscribes. Zero, one, or many handlers can react to the same event type. If no handler is registered, the event is silently discarded.

This implements the [Publish-Subscribe Channel](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PublishSubscribeChannel.html) pattern.

**Naming convention:** Name events using noun-verb past tense. `OrderPlaced`, `PaymentCompleted`, `UserRegistered`. The past tense signals that something already happened - the publisher is not directing anyone to act.

<MochaTopologyVisualization data='{"services":[{"host":{"serviceName":"OrderService","assemblyName":"OrderService.dll","instanceId":"order-svc-1"},"messageTypes":[{"identity":"msg:OrderPlacedEvent","runtimeType":"OrderPlacedEvent","runtimeTypeFullName":"MyApp.Messages.OrderPlacedEvent","isInterface":false,"isInternal":false}],"consumers":[{"name":"BillingHandler","identityType":"BillingHandler","identityTypeFullName":"MyApp.Handlers.BillingHandler"},{"name":"NotificationHandler","identityType":"NotificationHandler","identityTypeFullName":"MyApp.Handlers.NotificationHandler"}],"routes":{"inbound":[{"kind":"subscribe","messageTypeIdentity":"msg:OrderPlacedEvent","consumerName":"BillingHandler","endpoint":{"name":"billing-handler","address":"loopback://localhost/q/billing-handler","transportName":"InMemory"}},{"kind":"subscribe","messageTypeIdentity":"msg:OrderPlacedEvent","consumerName":"NotificationHandler","endpoint":{"name":"notification-handler","address":"loopback://localhost/q/notification-handler","transportName":"InMemory"}}],"outbound":[{"kind":"publish","messageTypeIdentity":"msg:OrderPlacedEvent","endpoint":{"name":"OrderPlacedEvent","address":"loopback://localhost/c/OrderPlacedEvent","transportName":"InMemory"}}]},"sagas":[]}],"transports":[{"identifier":"inmemory","name":"InMemory","schema":"loopback","transportType":"InMemoryTransport","receiveEndpoints":[{"name":"billing-handler","kind":"default","address":"loopback://localhost/q/billing-handler","source":{"address":"loopback://localhost/q/billing-handler"}},{"name":"notification-handler","kind":"default","address":"loopback://localhost/q/notification-handler","source":{"address":"loopback://localhost/q/notification-handler"}}],"dispatchEndpoints":[{"name":"OrderPlacedEvent","kind":"default","address":"loopback://localhost/c/OrderPlacedEvent","destination":{"address":"loopback://localhost/c/OrderPlacedEvent"}}],"topology":{"address":"loopback://localhost","entities":[{"kind":"channel","name":"OrderPlacedEvent","address":"loopback://localhost/c/OrderPlacedEvent","flow":"inbound","properties":{"type":"publish"}},{"kind":"queue","name":"billing-handler","address":"loopback://localhost/q/billing-handler","flow":"outbound","properties":{}},{"kind":"queue","name":"notification-handler","address":"loopback://localhost/q/notification-handler","flow":"outbound","properties":{}}],"links":[{"kind":"subscription","address":"loopback://localhost/sub/OrderPlacedEvent-billing-handler","source":"loopback://localhost/c/OrderPlacedEvent","target":"loopback://localhost/q/billing-handler","direction":"forward","properties":{}},{"kind":"subscription","address":"loopback://localhost/sub/OrderPlacedEvent-notification-handler","source":"loopback://localhost/c/OrderPlacedEvent","target":"loopback://localhost/q/notification-handler","direction":"forward","properties":{}}]}}]}' trace='{"traceId":"event-pubsub-trace-001","activities":[{"id":"ep-1","parentId":null,"startTime":"2024-06-15T10:30:00.000Z","durationMs":45,"status":"ok","operation":"publish","messageType":"OrderPlacedEvent","messageTypeIdentity":"msg:OrderPlacedEvent","transport":"InMemory"},{"id":"ep-2","parentId":"ep-1","startTime":"2024-06-15T10:30:00.001Z","durationMs":2,"status":"ok","operation":"dispatch","messageType":"OrderPlacedEvent","messageTypeIdentity":"msg:OrderPlacedEvent","endpointName":"OrderPlacedEvent","endpointAddress":"loopback://localhost/c/OrderPlacedEvent","transport":"InMemory"},{"id":"ep-3","parentId":"ep-2","startTime":"2024-06-15T10:30:00.010Z","durationMs":3,"status":"ok","operation":"receive","messageType":"OrderPlacedEvent","messageTypeIdentity":"msg:OrderPlacedEvent","endpointName":"billing-handler","endpointAddress":"loopback://localhost/q/billing-handler","transport":"InMemory"},{"id":"ep-4","parentId":"ep-3","startTime":"2024-06-15T10:30:00.013Z","durationMs":12,"status":"ok","operation":"consume","messageType":"OrderPlacedEvent","messageTypeIdentity":"msg:OrderPlacedEvent","consumerName":"BillingHandler"},{"id":"ep-5","parentId":"ep-2","startTime":"2024-06-15T10:30:00.012Z","durationMs":3,"status":"ok","operation":"receive","messageType":"OrderPlacedEvent","messageTypeIdentity":"msg:OrderPlacedEvent","endpointName":"notification-handler","endpointAddress":"loopback://localhost/q/notification-handler","transport":"InMemory"},{"id":"ep-6","parentId":"ep-5","startTime":"2024-06-15T10:30:00.015Z","durationMs":8,"status":"ok","operation":"consume","messageType":"OrderPlacedEvent","messageTypeIdentity":"msg:OrderPlacedEvent","consumerName":"NotificationHandler"}]}' />

## Publish an event and handle it

By the end of this section, you will have two independent handlers both processing the same published event.

### Define the event

```csharp
namespace MyApp.Messages;

public sealed record OrderPlacedEvent
{
    public required Guid OrderId { get; init; }
    public required string CustomerId { get; init; }
    public required decimal TotalAmount { get; init; }
    public required DateTimeOffset CreatedAt { get; init; }
}
```

### Implement two handlers

```csharp
using Mocha;

namespace MyApp.Handlers;

// Handler 1: Create an invoice when an order is placed
public class BillingHandler(ILogger<BillingHandler> logger)
    : IEventHandler<OrderPlacedEvent>
{
    public async ValueTask HandleAsync(
        OrderPlacedEvent message,
        CancellationToken cancellationToken)
    {
        logger.LogInformation(
            "Creating invoice for order {OrderId}, amount {Amount}",
            message.OrderId,
            message.TotalAmount);

        // Create invoice logic here
        await Task.CompletedTask;
    }
}

// Handler 2: Send a notification when an order is placed
public class NotificationHandler(ILogger<NotificationHandler> logger)
    : IEventHandler<OrderPlacedEvent>
{
    public async ValueTask HandleAsync(
        OrderPlacedEvent message,
        CancellationToken cancellationToken)
    {
        logger.LogInformation(
            "Sending confirmation to customer {CustomerId} for order {OrderId}",
            message.CustomerId,
            message.OrderId);

        // Send email/SMS logic here
        await Task.CompletedTask;
    }
}
```

Each handler implements `IEventHandler<OrderPlacedEvent>`. Both receive the same event independently. If one handler fails, the other still processes its copy.

### Register and publish

```csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<BillingHandler>()
    .AddEventHandler<NotificationHandler>()
    .AddInMemory();
```

From an endpoint, publish the event through the injected bus:

```csharp
app.MapPost("/orders", async (IMessageBus bus) =>
{
    await bus.PublishAsync(new OrderPlacedEvent
    {
        OrderId = Guid.NewGuid(),
        CustomerId = "customer-42",
        TotalAmount = 149.99m,
        CreatedAt = DateTimeOffset.UtcNow
    }, CancellationToken.None);

    return Results.Ok();
});
```

Expected output:

```text
info: MyApp.Handlers.BillingHandler[0]
      Creating invoice for order 3f2504e0-4f89-11d3-9a0c-0305e82c3301, amount 149.99
info: MyApp.Handlers.NotificationHandler[0]
      Sending confirmation to customer customer-42 for order 3f2504e0-4f89-11d3-9a0c-0305e82c3301
```

Both handlers execute. The order of execution is not guaranteed.

## How to chain events across services

A common pattern is for a handler to publish a new event after completing its work. This creates an event chain that coordinates multiple services without coupling them.

```csharp
public class OrderPlacedEventHandler(
    BillingDbContext db,
    IMessageBus messageBus,
    ILogger<OrderPlacedEventHandler> logger) : IEventHandler<OrderPlacedEvent>
{
    public async ValueTask HandleAsync(
        OrderPlacedEvent message,
        CancellationToken cancellationToken)
    {
        logger.LogInformation(
            "Order placed: {OrderId}, creating invoice",
            message.OrderId);

        // Create and process payment...

        // Publish a downstream event
        await messageBus.PublishAsync(
            new PaymentCompletedEvent
            {
                PaymentId = Guid.NewGuid(),
                OrderId = message.OrderId,
                Amount = message.TotalAmount,
                PaymentMethod = "CreditCard",
                ProcessedAt = DateTimeOffset.UtcNow
            },
            cancellationToken);
    }
}
```

The billing service handles `OrderPlacedEvent` and publishes `PaymentCompletedEvent`. A shipping service can subscribe to `PaymentCompletedEvent` without knowing about billing. Each service reacts to events it cares about.

# Send (fire-and-forget)

A send represents an instruction directed at a specific handler. Unlike events, a send has exactly one handler. The sender knows what it wants done but does not wait for a typed response - the message either succeeds or faults.

This implements the [Command Message](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CommandMessage.html) pattern.

**Naming convention:** Name send messages using verb-noun present tense. `ReserveInventory`, `ProcessPayment`, `ScheduleShipment`. The imperative form signals intent - you are telling a specific service what to do.

Use send for fire-and-forget operations: reserving inventory, scheduling a job, triggering a side effect in another service.

<MochaTopologyVisualization data='{"services":[{"host":{"serviceName":"OrderService","assemblyName":"OrderService.dll","instanceId":"order-svc-1"},"messageTypes":[{"identity":"msg:ReserveInventoryCommand","runtimeType":"ReserveInventoryCommand","runtimeTypeFullName":"MyApp.Messages.ReserveInventoryCommand","isInterface":false,"isInternal":false}],"consumers":[],"routes":{"inbound":[],"outbound":[{"kind":"send","messageTypeIdentity":"msg:ReserveInventoryCommand","endpoint":{"name":"inventory-handler","address":"loopback://localhost/q/inventory-handler","transportName":"InMemory"}}]},"sagas":[]},{"host":{"serviceName":"InventoryService","assemblyName":"InventoryService.dll","instanceId":"inventory-svc-1"},"messageTypes":[{"identity":"msg:ReserveInventoryCommand","runtimeType":"ReserveInventoryCommand","runtimeTypeFullName":"MyApp.Messages.ReserveInventoryCommand","isInterface":false,"isInternal":false}],"consumers":[{"name":"ReserveInventoryCommandHandler","identityType":"ReserveInventoryCommandHandler","identityTypeFullName":"MyApp.Handlers.ReserveInventoryCommandHandler"}],"routes":{"inbound":[{"kind":"request","messageTypeIdentity":"msg:ReserveInventoryCommand","consumerName":"ReserveInventoryCommandHandler","endpoint":{"name":"inventory-handler","address":"loopback://localhost/q/inventory-handler","transportName":"InMemory"}}],"outbound":[]},"sagas":[]}],"transports":[{"identifier":"inmemory","name":"InMemory","schema":"loopback","transportType":"InMemoryTransport","receiveEndpoints":[{"name":"inventory-handler","kind":"default","address":"loopback://localhost/q/inventory-handler","source":{"address":"loopback://localhost/q/inventory-handler"}}],"dispatchEndpoints":[{"name":"inventory-handler","kind":"default","address":"loopback://localhost/q/inventory-handler","destination":{"address":"loopback://localhost/q/inventory-handler"}}],"topology":{"address":"loopback://localhost","entities":[{"kind":"queue","name":"inventory-handler","address":"loopback://localhost/q/inventory-handler","flow":"inbound","properties":{"type":"send"}}],"links":[]}}]}' trace='{"traceId":"command-send-trace-001","activities":[{"id":"cs-1","parentId":null,"startTime":"2024-06-15T10:30:00.000Z","durationMs":30,"status":"ok","operation":"send","messageType":"ReserveInventoryCommand","messageTypeIdentity":"msg:ReserveInventoryCommand","transport":"InMemory"},{"id":"cs-2","parentId":"cs-1","startTime":"2024-06-15T10:30:00.001Z","durationMs":2,"status":"ok","operation":"dispatch","messageType":"ReserveInventoryCommand","messageTypeIdentity":"msg:ReserveInventoryCommand","endpointName":"inventory-handler","endpointAddress":"loopback://localhost/q/inventory-handler","transport":"InMemory"},{"id":"cs-3","parentId":"cs-2","startTime":"2024-06-15T10:30:00.008Z","durationMs":3,"status":"ok","operation":"receive","messageType":"ReserveInventoryCommand","messageTypeIdentity":"msg:ReserveInventoryCommand","endpointName":"inventory-handler","endpointAddress":"loopback://localhost/q/inventory-handler","transport":"InMemory"},{"id":"cs-4","parentId":"cs-3","startTime":"2024-06-15T10:30:00.011Z","durationMs":15,"status":"ok","operation":"consume","messageType":"ReserveInventoryCommand","messageTypeIdentity":"msg:ReserveInventoryCommand","consumerName":"ReserveInventoryCommandHandler"}]}' />

> [!WARNING]
> **Send in disguise.** If your "event" expects exactly one handler to take action, it should be a send. Use `SendAsync`, not `PublishAsync`. Publishing a message that requires a single specific handler breaks the semantic contract of events and makes the system harder to reason about.

## Send a message and handle it

By the end of this section, you will send a message to a single handler and verify it executes.

### Define the message

```csharp
namespace MyApp.Messages;

public sealed record ReserveInventoryCommand
{
    public required Guid OrderId { get; init; }
    public required Guid ProductId { get; init; }
    public required int Quantity { get; init; }
}
```

### Implement the handler

```csharp
using Mocha;

namespace MyApp.Handlers;

public class ReserveInventoryCommandHandler(
    AppDbContext db,
    ILogger<ReserveInventoryCommandHandler> logger)
    : IEventRequestHandler<ReserveInventoryCommand>
{
    public async ValueTask HandleAsync(
        ReserveInventoryCommand request,
        CancellationToken cancellationToken)
    {
        logger.LogInformation(
            "Reserving {Quantity} units of product {ProductId} for order {OrderId}",
            request.Quantity,
            request.ProductId,
            request.OrderId);

        var product = await db.Products
            .FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken);

        if (product is null)
        {
            throw new InvalidOperationException(
                $"Product {request.ProductId} not found");
        }

        product.StockQuantity -= request.Quantity;
        await db.SaveChangesAsync(cancellationToken);

        logger.LogInformation(
            "Reserved {Quantity} units. Remaining stock: {Remaining}",
            request.Quantity,
            product.StockQuantity);
    }
}
```

`IEventRequestHandler<TRequest>` (single type parameter) handles the command without returning a value. If the handler throws, the bus generates a fault.

### Register and send

```csharp
builder.Services
    .AddMessageBus()
    .AddRequestHandler<ReserveInventoryCommandHandler>()
    .AddInMemory();
```

Send the command:

```csharp
await bus.SendAsync(new ReserveInventoryCommand
{
    OrderId = Guid.NewGuid(),
    ProductId = productId,
    Quantity = 3
}, cancellationToken);
```

Expected output:

```text
info: MyApp.Handlers.ReserveInventoryCommandHandler[0]
      Reserving 3 units of product a1b2c3d4-... for order e5f6a7b8-...
info: MyApp.Handlers.ReserveInventoryCommandHandler[0]
      Reserved 3 units. Remaining stock: 97
```

`SendAsync` completes after the message is dispatched to the transport. It does not wait for the handler to finish processing.

## How to wait for command acknowledgment

When you need confirmation that the handler processed the command, use `RequestAsync` instead of `SendAsync`. Mocha sends an automatic `AcknowledgedEvent` when the handler completes.

```csharp
// Fire-and-forget: returns after dispatch
await bus.SendAsync(command, cancellationToken);

// Wait for acknowledgment: returns after the handler completes
await bus.RequestAsync(command, cancellationToken);
```

The `RequestAsync` overload that accepts `object` (no `IEventRequest<T>` constraint) waits for the handler's acknowledgment without expecting a typed response. If the handler throws, `RequestAsync` throws a `ResponseTimeoutException`.

# Request/Reply

Request/reply is for when the sender needs a typed response. The sender dispatches a request, Mocha routes it to a handler, the handler returns a response, and Mocha delivers the response back to the sender. The entire round trip completes within a single `await`.

This implements the [Request-Reply](https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html) pattern.

When you call `RequestAsync`, Mocha creates a temporary reply address and embeds it in the request envelope. The handler reads the reply address from the envelope and sends the response back to it. A correlation ID links the request and response across the transport. This is what makes `ResponseTimeoutException` possible - if the reply never arrives at the temporary address, the timeout fires.

> [!NOTE]
> The reply address is generated and managed internally by Mocha for request/reply correlation - it is not a receive endpoint you configure. To scope your own receive endpoint's infrastructure to the lifetime of the process, call `Temporary()` on a queue or endpoint descriptor. See [Temporary endpoints and per-instance identity](./routing-and-endpoints.md#temporary-endpoints-and-per-instance-identity).

<MochaTopologyVisualization data='{"services":[{"host":{"serviceName":"RequestReply","assemblyName":"RequestReply","instanceId":"de1ab3cc-3802-44af-8523-b316a57228d1"},"messageTypes":[{"identity":"urn:message:mocha.events:not-acknowledged-event","runtimeType":"NotAcknowledgedEvent","runtimeTypeFullName":"Mocha.Events.NotAcknowledgedEvent","isInterface":false,"isInternal":true,"enclosedMessageIdentities":["urn:message:mocha.events:not-acknowledged-event"]},{"identity":"urn:message:mocha.events:acknowledged-event","runtimeType":"AcknowledgedEvent","runtimeTypeFullName":"Mocha.Events.AcknowledgedEvent","isInterface":false,"isInternal":true,"enclosedMessageIdentities":["urn:message:mocha.events:acknowledged-event"]},{"identity":"urn:message:global:process-refund-command","runtimeType":"ProcessRefundCommand","runtimeTypeFullName":"ProcessRefundCommand","isInterface":false,"isInternal":false,"enclosedMessageIdentities":["urn:message:global:process-refund-command","urn:message:mocha:i-event-request[process-refund-response]","urn:message:mocha:i-event-request"]},{"identity":"urn:message:global:process-refund-response","runtimeType":"ProcessRefundResponse","runtimeTypeFullName":"ProcessRefundResponse","isInterface":false,"isInternal":false,"enclosedMessageIdentities":["urn:message:global:process-refund-response"]}],"consumers":[{"name":"Reply","identityType":"ReplyConsumer","identityTypeFullName":"Mocha.ReplyConsumer","isBatch":false},{"name":"ProcessRefundCommandHandler","identityType":"ProcessRefundCommandHandler","identityTypeFullName":"ProcessRefundCommandHandler","isBatch":false}],"routes":{"inbound":[{"kind":"request","messageTypeIdentity":"urn:message:global:process-refund-command","consumerName":"ProcessRefundCommandHandler","endpoint":{"name":"process-refund","address":"memory://localhost/process-refund","transportName":"memory"}},{"kind":"reply","consumerName":"Reply","endpoint":{"name":"Replies","address":"memory://localhost/Replies","transportName":"memory"}}],"outbound":[{"kind":"send","messageTypeIdentity":"urn:message:global:process-refund-command","endpoint":{"name":"q/process-refund","address":"memory://localhost/q/process-refund","transportName":"memory"}}]},"sagas":[]}],"transports":[{"identifier":"memory://requestreply/","name":"memory","schema":"memory","transportType":"InMemoryMessagingTransport","receiveEndpoints":[{"name":"Replies","kind":"reply","address":"memory://localhost/Replies","source":{"address":"memory://requestreply/q/response-de1ab3cc380244af8523b316a57228d1"}},{"name":"process-refund","kind":"default","address":"memory://localhost/process-refund","source":{"address":"memory://requestreply/q/process-refund"}}],"dispatchEndpoints":[{"name":"Replies","kind":"reply","address":"memory://localhost/Replies","destination":{"address":"memory://requestreply/q/response-de1ab3cc380244af8523b316a57228d1"}},{"name":"q/process-refund","kind":"default","address":"memory://localhost/q/process-refund","destination":{"address":"memory://requestreply/q/process-refund"}}],"topology":{"address":"memory://requestreply/","entities":[{"kind":"queue","name":"process-refund","address":"memory://requestreply/q/process-refund","flow":"outbound"}],"links":[{"kind":"bind","address":"memory://requestreply/b/t/.process-refund/t/process-refund","source":"memory://requestreply/e/.process-refund","target":"memory://requestreply/e/process-refund","direction":"forward"},{"kind":"bind","address":"memory://requestreply/b/t/process-refund/q/process-refund","source":"memory://requestreply/e/process-refund","target":"memory://requestreply/q/process-refund","direction":"forward"}]}}]}' trace='{"traceId":"request-reply-trace-001","activities":[{"id":"rr-1","parentId":null,"startTime":"2024-06-15T10:30:00.000Z","durationMs":60,"status":"ok","operation":"send","messageType":"ProcessRefundCommand","messageTypeIdentity":"urn:message:global:process-refund-command","transport":"memory"},{"id":"rr-2","parentId":"rr-1","startTime":"2024-06-15T10:30:00.001Z","durationMs":2,"status":"ok","operation":"dispatch","messageType":"ProcessRefundCommand","messageTypeIdentity":"urn:message:global:process-refund-command","endpointName":"q/process-refund","endpointAddress":"memory://localhost/q/process-refund","transport":"memory"},{"id":"rr-3","parentId":"rr-2","startTime":"2024-06-15T10:30:00.008Z","durationMs":3,"status":"ok","operation":"receive","messageType":"ProcessRefundCommand","messageTypeIdentity":"urn:message:global:process-refund-command","endpointName":"process-refund","endpointAddress":"memory://localhost/process-refund","transport":"memory"},{"id":"rr-4","parentId":"rr-3","startTime":"2024-06-15T10:30:00.011Z","durationMs":20,"status":"ok","operation":"consume","messageType":"ProcessRefundCommand","messageTypeIdentity":"urn:message:global:process-refund-command","consumerName":"ProcessRefundCommandHandler"},{"id":"rr-5","parentId":"rr-4","startTime":"2024-06-15T10:30:00.028Z","durationMs":2,"status":"ok","operation":"reply","messageType":"ProcessRefundResponse","messageTypeIdentity":"urn:message:global:process-refund-response","transport":"memory","conversationId":"conv-rr-001"},{"id":"rr-6","parentId":"rr-5","startTime":"2024-06-15T10:30:00.031Z","durationMs":2,"status":"ok","operation":"dispatch","messageType":"ProcessRefundResponse","messageTypeIdentity":"urn:message:global:process-refund-response","endpointName":"Replies","endpointAddress":"memory://localhost/Replies","transport":"memory"},{"id":"rr-7","parentId":"rr-6","startTime":"2024-06-15T10:30:00.038Z","durationMs":3,"status":"ok","operation":"receive","messageType":"ProcessRefundResponse","messageTypeIdentity":"urn:message:global:process-refund-response","endpointName":"Replies","endpointAddress":"memory://localhost/Replies","transport":"memory"}]}' />

## Send a request and await a response

By the end of this section, you will send a request to a handler and use the typed response.

### Define the request and response

The request record must implement `IEventRequest<TResponse>`. This marker interface tells Mocha the expected response type and enables compile-time correlation.

```csharp
using Mocha;

namespace MyApp.Messages;

public sealed record ProcessRefundCommand : IEventRequest<ProcessRefundResponse>
{
    public required Guid OrderId { get; init; }
    public required decimal Amount { get; init; }
    public required string Reason { get; init; }
    public required string CustomerId { get; init; }
}

public sealed record ProcessRefundResponse
{
    public required Guid RefundId { get; init; }
    public required Guid OrderId { get; init; }
    public required decimal Amount { get; init; }
    public required bool Success { get; init; }
    public string? FailureReason { get; init; }
    public required DateTimeOffset ProcessedAt { get; init; }
}
```

### Implement the handler

```csharp
using Mocha;

namespace MyApp.Handlers;

public class ProcessRefundCommandHandler(
    BillingDbContext db,
    ILogger<ProcessRefundCommandHandler> logger)
    : IEventRequestHandler<ProcessRefundCommand, ProcessRefundResponse>
{
    public async ValueTask<ProcessRefundResponse> HandleAsync(
        ProcessRefundCommand request,
        CancellationToken cancellationToken)
    {
        logger.LogInformation(
            "Processing refund of {Amount} for order {OrderId}",
            request.Amount,
            request.OrderId);

        // Process refund logic...

        return new ProcessRefundResponse
        {
            RefundId = Guid.NewGuid(),
            OrderId = request.OrderId,
            Amount = request.Amount,
            Success = true,
            ProcessedAt = DateTimeOffset.UtcNow
        };
    }
}
```

`IEventRequestHandler<TRequest, TResponse>` requires `TRequest : IEventRequest<TResponse>`. The return value is sent back to the caller's reply endpoint. The return value must not be `null` - if you return `null`, the consumer throws an `InvalidOperationException`.

### Register and request

```csharp
builder.Services
    .AddMessageBus()
    .AddRequestHandler<ProcessRefundCommandHandler>()
    .AddInMemory();
```

Send the request and use the response:

```csharp
var response = await bus.RequestAsync(
    new ProcessRefundCommand
    {
        OrderId = orderId,
        Amount = 49.99m,
        Reason = "Defective product",
        CustomerId = "customer-42"
    },
    cancellationToken);

Console.WriteLine($"Refund {response.RefundId}: {response.Amount:C}, success={response.Success}");
```

Expected output:

```text
info: MyApp.Handlers.ProcessRefundCommandHandler[0]
      Processing refund of 49.99 for order e5f6a7b8-...
Refund d4c3b2a1-...: $49.99, success=True
```

## How to handle timeouts

If the handler does not respond within the configured timeout, `RequestAsync` throws a `ResponseTimeoutException`. You can catch this and handle it:

```csharp
try
{
    var response = await bus.RequestAsync(
        new ProcessRefundCommand
        {
            OrderId = orderId,
            Amount = 49.99m,
            Reason = "Defective product",
            CustomerId = "customer-42"
        },
        cancellationToken);

    // Use response...
}
catch (ResponseTimeoutException ex)
{
    logger.LogWarning(
        "Refund request timed out for order {OrderId}: {Message}",
        orderId,
        ex.Message);

    // Retry, fall back, or notify the user
}
```

Common causes of timeouts: the handler service is not running, the handler registration is missing, or the handler is taking longer than the timeout window.

## How to use `IEventRequest<TResponse>` correctly

The `IEventRequest<TResponse>` marker interface connects the request type to its response type at compile time. This enables two things:

1. **Type-safe `RequestAsync`.** The compiler infers `TResponse` from the request parameter, so `bus.RequestAsync(request)` returns `ValueTask<ProcessRefundResponse>` without you specifying the type.
2. **Handler constraint.** `IEventRequestHandler<TRequest, TResponse>` requires `TRequest : IEventRequest<TResponse>`, preventing mismatched request/response pairings at compile time.

```csharp
// The compiler infers TResponse = ProcessRefundResponse
// because ProcessRefundCommand : IEventRequest<ProcessRefundResponse>
var response = await bus.RequestAsync(refundCommand, cancellationToken);
```

If your request record does not implement `IEventRequest<TResponse>`, you cannot use the typed `RequestAsync<TResponse>` overload.

# When to use which pattern

| Question                             | Event (`PublishAsync`) | Send (`SendAsync`)        | Request/Reply (`RequestAsync`)  |
| ------------------------------------ | ---------------------- | ------------------------- | ------------------------------- |
| How many handlers?                   | Zero or more           | Exactly one               | Exactly one                     |
| Does the sender need a response?     | No                     | No                        | Yes                             |
| Does the sender know who handles it? | No                     | Yes (by routing)          | Yes (by routing)                |
| Does the sender wait for processing? | No                     | No                        | Yes                             |
| Handler interface                    | `IEventHandler<T>`     | `IEventRequestHandler<T>` | `IEventRequestHandler<T, TRes>` |

**Use events** when multiple parts of the system need to react to something that happened. The publisher does not care who listens or what they do with the event. Examples: order placed, payment completed, user signed up.

**Use send** when you need to tell a specific service to do something but do not need a result back. The sender knows the operation should happen but does not need to wait for it. Examples: reserve inventory, send an email, schedule a cleanup job.

**Use request/reply** when the sender needs data back or needs to know whether the operation succeeded with details. The call blocks until the response arrives. Examples: process a refund and get the refund ID, look up product details, validate an address.

# See also

- [Microsoft Azure Architecture: Publisher-Subscriber Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber) - When to use pub/sub and when not to, including considerations for idempotency and message ordering.
- [CodeOpinion: Commands & Events - What's the difference?](https://codeopinion.com/commands-events-whats-the-difference/) - A concise explanation of the semantic distinction between commands and events.
- [EIP: Publish-Subscribe Channel](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PublishSubscribeChannel.html) - The canonical definition of the pub/sub pattern.
- [EIP: Command Message](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CommandMessage.html) - Commands as a way to invoke behavior in another service via messaging.
- [EIP: Request-Reply](https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html) - Correlation IDs and reply queues explained.

> **Runnable examples:** [EventPubSub](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/MessagingPatterns/EventPubSub), [SendFireAndForget](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/MessagingPatterns/SendFireAndForget), [RequestReply](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/MessagingPatterns/RequestReply)
>
> **Full demo:** The [Demo application](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo) uses all three patterns: [Demo.Catalog](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Catalog) publishes `OrderPlacedEvent` (pub/sub), [Demo.Billing](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Billing) handles `ProcessRefundCommand` (send), and sagas use `RequestAsync` for request/reply coordination.

Ready to implement these patterns? See [Handlers and Consumers](./handlers-and-consumers.md).

---

### Website Content Docs Mocha Middleware And Pipelines (`website/content/docs/mocha/middleware-and-pipelines.md`)

---
title: "Middleware and Pipelines"
description: "Understand how Mocha's three middleware pipelines process messages, which pipeline to target, and how to write and order custom middleware."
---

Mocha's pipeline implements the [Pipes and Filters](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PipesAndFilters.html) pattern. Every message flows through a chain of middleware before reaching your handler. Each filter in the chain can inspect, modify, short-circuit, or observe the message - then pass control to the next filter.

If you have used middleware in [ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/), the mental model is the same. Middleware wraps middleware in nested layers, like an onion. Each layer runs code before calling the next layer, and optionally runs more code after it returns.

Most of the time, the defaults work and you never configure middleware directly. This page is for when you need to add cross-cutting behavior - a database transaction per handler, tenant context from headers, custom rate limiting - or when you want to understand how the built-in reliability and observability features fit into the pipeline.

# How pipelines work

## The nesting model

Within a single pipeline, each middleware wraps the next. Execution flows inward through each layer, then unwinds outward as each layer returns:

```text
Message arrives
  → MiddlewareA.InvokeAsync
      → MiddlewareB.InvokeAsync
          → MiddlewareC.InvokeAsync
              → [handler or next pipeline]
          ← return (or exception propagates back here)
      ← return (or exception caught/rethrown here)
  ← return
```

A middleware that does not call `next(context)` short-circuits the pipeline - everything after it is skipped. The expiry middleware uses this to drop stale messages without invoking any downstream code. A middleware that wraps `next` in a try/catch can intercept exceptions from downstream - the fault middleware uses this to route failed messages to an error endpoint.

This wrapping model is why registration order matters: the first middleware registered becomes the outermost layer. It runs first on the way in, and last on the way out.

## The three pipelines

Mocha has three separate pipelines with different context types and different purposes:

```text
PublishAsync / SendAsync / RequestAsync
        │
        ▼
┌─────────────────────────┐
│   Dispatch Pipeline      │
│  Instrumentation         │
│  Serialization           │
│  → Transport sends       │
└─────────────────────────┘
        │
        ▼  (wire / InMemory)
        │
┌─────────────────────────┐
│   Receive Pipeline       │
│  TransportCircuitBreaker │
│  ConcurrencyLimiter      │
│  ReceiveInstrumentation  │
│  DeadLetter              │
│  Fault                   │
│  CircuitBreaker          │
│  Expiry                  │
│  MessageTypeSelection    │
│  Routing                 │
└─────────────────────────┘
        │
        ▼
┌─────────────────────────┐
│   Consumer Pipeline      │
│  Instrumentation         │
│  Transaction (optional)  │
│  Inbox (optional)        │
│  → Your Handler          │
└─────────────────────────┘
```

Each pipeline operates on a different context:

- **Dispatch** (`IDispatchContext`) - operates on the unserialized message object. Has the CLR message, headers, and destination address. Serialization happens at the end.
- **Receive** (`IReceiveContext`) - operates on the raw envelope from the transport. Has the serialized body, headers, and transport metadata. Message type resolution and routing happen here.
- **Consumer** (`IConsumeContext`) - operates on the deserialized message inside a specific consumer. Has the typed message, envelope metadata, and scoped services.

In a distributed system, the dispatch and receive pipelines run in different processes. With the InMemory transport, they run in the same process.

# Which pipeline should I target?

| Use case                                           | Pipeline                             |
| -------------------------------------------------- | ------------------------------------ |
| Add a header to every outgoing message             | Dispatch                             |
| Validate messages before sending                   | Dispatch                             |
| Rate-limit incoming messages at the endpoint level | Receive                              |
| Database transaction wrapping every handler        | Consumer                             |
| Time individual handler execution                  | Consumer                             |
| Extract tenant context from message headers        | Receive (before Routing) or Consumer |

# Consumer middleware

Consumer middleware wraps your handler execution. It is the most common customization point - use it for cross-cutting concerns that apply to every handler: database transactions, validation, timing, or tenant context resolution.

## Database unit-of-work example

A database transaction that commits on success and rolls back on failure is the canonical consumer middleware use case:

```csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Mocha;
using Mocha.Middlewares;

internal sealed class UnitOfWorkConsumerMiddleware
{
    public async ValueTask InvokeAsync(
        IConsumeContext context,
        ConsumerDelegate next)
    {
        // Resolve DbContext from the per-message DI scope
        var db = context.Services.GetRequiredService<AppDbContext>();

        await using var tx = await db.Database.BeginTransactionAsync();
        try
        {
            await next(context);       // Run the handler
            await tx.CommitAsync();    // Commit on success
        }
        catch
        {
            await tx.RollbackAsync();  // Roll back on any handler exception
            throw;                     // Re-throw so fault middleware can route the message
        }
    }

    public static ConsumerMiddlewareConfiguration Create()
        => new(
            static (context, next) =>
            {
                var middleware = new UnitOfWorkConsumerMiddleware();
                return ctx => middleware.InvokeAsync(ctx, next);
            },
            "UnitOfWork");
}
```

Register the middleware during bus configuration:

```csharp
builder.Services
    .AddMessageBus(bus =>
    {
        bus.UseConsume(UnitOfWorkConsumerMiddleware.Create());
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddInMemory();
```

The re-throw after `RollbackAsync` is intentional. The fault middleware further up the receive pipeline catches handler exceptions and routes the message to an error endpoint. If you swallow the exception, the message is silently dropped.

# Receive middleware

Receive middleware wraps the entire receive pipeline for a message, before message type resolution and routing. Use it for concerns that apply to raw envelopes: rate limiting, logging of all incoming messages, or metrics about transport-level behavior.

```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Mocha;
using Mocha.Middlewares;

internal sealed class LoggingReceiveMiddleware(
    ILogger<LoggingReceiveMiddleware> logger)
{
    public async ValueTask InvokeAsync(
        IReceiveContext context,
        ReceiveDelegate next)
    {
        logger.LogInformation(
            "Receiving {MessageId}",
            context.MessageId);

        await next(context);

        logger.LogInformation(
            "Finished {MessageId}",
            context.MessageId);
    }

    public static ReceiveMiddlewareConfiguration Create()
        => new(
            static (context, next) =>
            {
                var logger = context.Services
                    .GetRequiredService<ILogger<LoggingReceiveMiddleware>>();
                var middleware = new LoggingReceiveMiddleware(logger);
                return ctx => middleware.InvokeAsync(ctx, next);
            },
            "Logging");
}
```

Register after instrumentation so telemetry spans are already open when your middleware runs:

```csharp
builder.Services
    .AddMessageBus(bus =>
    {
        // Insert after ReceiveInstrumentation, before DeadLetter
        bus.UseReceive(
            LoggingReceiveMiddleware.Create(),
            after: "ReceiveInstrumentation");
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddInMemory();
```

`#!csharp UseReceive(..., after: "ReceiveInstrumentation")` places your middleware immediately after the named middleware. The receive pipeline then executes: TransportCircuitBreaker, ConcurrencyLimiter, ReceiveInstrumentation, **Logging**, DeadLetter, Fault, and so on.

# Dispatch middleware

Dispatch middleware wraps the outbound path. Use it for concerns on every outgoing message: adding headers, enforcing message schemas, or enriching with correlation context.

```csharp
internal sealed class TenantDispatchMiddleware
{
    private readonly string _tenantId;

    public TenantDispatchMiddleware(string tenantId)
    {
        _tenantId = tenantId;
    }

    public async ValueTask InvokeAsync(
        IDispatchContext context,
        DispatchDelegate next)
    {
        // Stamp every outgoing message with the tenant identifier
        context.Headers.Set("x-tenant", _tenantId);

        await next(context);
    }

    public static DispatchMiddlewareConfiguration Create(string tenantId)
        => new(
            (context, next) =>
            {
                var middleware = new TenantDispatchMiddleware(tenantId);
                return ctx => middleware.InvokeAsync(ctx, next);
            },
            "TenantDispatch");
}
```

Register before all other dispatch middleware so every outgoing message carries the header:

```csharp
builder.Services
    .AddMessageBus(bus =>
    {
        bus.UseDispatch(
            TenantDispatchMiddleware.Create("acme"),
            before: "Instrumentation");
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddInMemory();
```

# The factory pattern and DI scoping

The factory lambda in `ReceiveMiddlewareConfiguration`, `ConsumerMiddlewareConfiguration`, and `DispatchMiddlewareConfiguration` runs once at startup to build the pipeline delegate. Only the innermost delegate (your middleware's `InvokeAsync`) runs per message. Services resolved inside the innermost delegate come from the request-scoped DI container for that message. You do not want middleware instances created per message if possible - resolve services from `context.Services` inside `InvokeAsync` instead.

> [!WARNING]
> **Avoid capturing services outside the lambda.** If you resolve a service outside the factory lambda, it is shared across all messages and behaves as a singleton - even if it was registered as scoped. This breaks scoped services like `DbContext`:
>
> ```csharp
> // Wrong: DbContext captured as a singleton
> var db = serviceProvider.GetRequiredService<AppDbContext>();
> return new ConsumerMiddlewareConfiguration(
>     (context, next) =>
>         ctx => middleware.InvokeAsync(ctx, next, db), // db is shared!
>     "UnitOfWork");
>
> // Correct: resolve inside the per-message factory
> return new ConsumerMiddlewareConfiguration(
>     static (context, next) =>
>     {
>         var db = context.Services.GetRequiredService<AppDbContext>();
>         var middleware = new UnitOfWorkConsumerMiddleware();
>         return ctx => middleware.InvokeAsync(ctx, next);
>     },
>     "UnitOfWork");
> ```

# Control middleware ordering

Each pipeline type has a single registration method with optional `before` and `after` parameters:

| Method                               | Pipeline | Behavior                                        |
| ------------------------------------ | -------- | ----------------------------------------------- |
| `UseReceive(config)`                 | Receive  | Append to the pipeline                          |
| `UseReceive(config, before: "key")`  | Receive  | Insert before the middleware with the given key |
| `UseReceive(config, after: "key")`   | Receive  | Insert after the middleware with the given key  |
| `UseDispatch(config)`                | Dispatch | Append to the pipeline                          |
| `UseDispatch(config, before: "key")` | Dispatch | Insert before the middleware with the given key |
| `UseDispatch(config, after: "key")`  | Dispatch | Insert after the middleware with the given key  |
| `UseConsume(config)`                 | Consumer | Append to the pipeline                          |
| `UseConsume(config, before: "key")`  | Consumer | Insert before the middleware with the given key |
| `UseConsume(config, after: "key")`   | Consumer | Insert after the middleware with the given key  |

Only one of `before` or `after` can be specified at the same time. If neither is specified, the middleware is appended after the built-in defaults.

Middleware is compiled once at startup into a single delegate chain. Register all middleware during bus configuration, before the service provider is built. Middleware added after the bus starts has no effect.

Middleware can also be registered at transport or endpoint scope. Bus-level middleware applies to all transports and endpoints. Transport-level middleware applies to all endpoints on that transport. Endpoint-level middleware applies to a single endpoint. The most specific scope wins. This is the same scope hierarchy described in [Routing and Endpoints](./routing-and-endpoints.md).

# Built-in middleware and feature pages

The built-in middleware in the receive pipeline implements the reliability and observability features described on their own pages:

- The `Inbox` middleware deduplicates incoming messages based on `MessageId`, described in [Reliability](./reliability.md#deduplicate-messages-with-the-transactional-inbox). It runs in the **consumer pipeline** after the transaction middleware so that the inbox claim participates in the same database transaction as the handler's business data. Use `#!csharp UseConsume(config, before: "Inbox")` or `#!csharp UseConsume(config, after: "Inbox")` to position your middleware relative to it.
- The `CircuitBreaker` and `DeadLetter` middleware implement the circuit breaker and dead-letter behaviors described in [Reliability](./reliability.md). Use `UseReceive(config, before: "key")` or `UseReceive(config, after: "key")` with their keys to position your middleware relative to them.
- The `ReceiveInstrumentation` middleware generates the OpenTelemetry spans and metrics described in [Observability](./observability.md). Place logging or correlation middleware after `ReceiveInstrumentation` using `#!csharp UseReceive(config, after: "ReceiveInstrumentation")`.

# Next steps

The pipeline handles failures automatically. Learn how circuit breaking, dead-letter routing, the transactional outbox, and the idempotent inbox work in [Reliability](./reliability.md).

> **Runnable examples:** [CustomMiddleware](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Middleware/CustomMiddleware), [UnitOfWork](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Middleware/UnitOfWork)

---

### Website Content Docs Mocha Observability (`website/content/docs/mocha/observability.md`)

---
title: "Observability"
description: "Instrument Mocha with OpenTelemetry for distributed tracing, metrics, and logging across dispatch, receive, and consumer pipelines."
---

Mocha integrates with OpenTelemetry to give you distributed traces, metrics, and structured logging across every stage of the messaging pipeline. When a message is dispatched, received, or consumed, Mocha emits spans and metrics that let you trace message flows end-to-end across services, measure throughput, and diagnose failures.

Observability is opt-in. Call `.AddInstrumentation()` on the bus builder to activate the built-in `OpenTelemetryDiagnosticObserver`. Without it, Mocha uses a no-op observer that adds zero overhead.

# Enable tracing and metrics

## Register instrumentation

```csharp
using Mocha;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddMessageBus()
    .AddInstrumentation() // Enables OpenTelemetry traces and metrics
    .AddEventHandler<OrderPlacedEventHandler>()
    .AddRabbitMQ();
```

`.AddInstrumentation()` registers the `OpenTelemetryDiagnosticObserver` as a singleton `IBusDiagnosticObserver`. This observer creates `Activity` spans for dispatch, receive, and consume operations, and records metrics via the `Mocha` meter.

## Subscribe to the Mocha activity source

The .NET OpenTelemetry SDK only collects spans from activity sources you explicitly subscribe to. Add the `"Mocha"` source to your tracing configuration:

```csharp
builder.Services
    .AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing
            .AddSource("Mocha") // Subscribe to Mocha spans
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation();
    })
    .WithMetrics(metrics =>
    {
        metrics
            .AddMeter("Mocha") // Subscribe to Mocha metrics
            .AddAspNetCoreInstrumentation();
    });
```

> [!NOTE]
> Mocha follows the [OpenTelemetry messaging semantic conventions](https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/), which are currently in development status. Attribute names may change in future OTel releases.

# What you will see

After publishing a message, open your tracing backend (Aspire Dashboard, Jaeger, or an OTLP collector). For each message flow you should see three linked spans:

1. **`publish {destination}`** - Created by the dispatch pipeline when the message is sent. This span belongs to the publishing service.
2. **`receive {endpoint}`** - Created by the receive pipeline when the message arrives at an endpoint. This span belongs to the consuming service and links back to the publish span.
3. **`consumer {handler}`** - Created as a child of the receive span when your handler processes the message.

In the Aspire Dashboard, you will see the `publish` span from the publishing service linked to the `receive` span in the consumer service, with a child `consumer` span for the handler execution. The three spans appear as a single distributed trace that crosses service boundaries.

# How trace context propagates

When the dispatch instrumentation middleware runs, it writes the current `Activity`'s trace context into the outgoing message headers using the [W3C Trace Context](https://www.w3.org/TR/trace-context/) standard (`traceparent` and `tracestate`). The receive instrumentation middleware on the other side reads those headers and restores the parent context, linking the two spans into a single trace. This is the same propagation format used by ASP.NET Core, HttpClient, and the broader OpenTelemetry ecosystem, so Mocha traces connect seamlessly with spans from other frameworks.

```mermaid
sequenceDiagram
    participant A as Service A
    participant Broker as RabbitMQ
    participant B as Service B

    A->>A: dispatch span (Producer)
    Note over A: Writes traceparent, tracestate to headers
    A->>Broker: Message + W3C trace headers
    Broker->>B: Deliver message
    B->>B: receive span (Client, linked to parent)
    B->>B: consumer span (Consumer)
```

Tracing and metrics are injected by middleware in all three pipelines: `DispatchInstrumentation`, `ReceiveInstrumentation`, and `ConsumerInstrumentation`. See [Middleware and Pipelines](./middleware-and-pipelines.md) for how these fit into the pipeline architecture and how to reorder or replace them.

# How to implement a custom diagnostic observer

To collect custom telemetry or integrate with a non-OpenTelemetry backend, implement `IBusDiagnosticObserver`. Each method is called at the start of its pipeline stage and returns an `IDisposable` that the pipeline disposes when the operation completes. This pattern lets you measure duration, track in-flight operations, and clean up resources.

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

Register your observer instead of (or alongside) the built-in one:

```csharp
builder.Services.AddSingleton<IBusDiagnosticObserver, CustomDiagnosticObserver>();
```

# Configure with Nitro

Nitro provides a managed telemetry backend for your Mocha services. Add the `ChilliCream.Nitro` and `ChilliCream.Nitro.OpenTelemetry` packages and configure the exporter:

```shell
dotnet add package ChilliCream.Nitro
dotnet add package ChilliCream.Nitro.OpenTelemetry
```

```csharp
builder.Services
    .AddNitro()
    .AddOpenTelemetry();

builder.Services
    .AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.AddSource("Mocha");
    })
    .WithMetrics(metrics =>
    {
        metrics.AddMeter("Mocha");
    });

builder.Logging.AddOpenTelemetry(logging =>
{
    logging.IncludeFormattedMessage = true;
    logging.IncludeScopes = true;
});
```

Configure credentials through environment variables:

| Variable        | Purpose                                          |
| --------------- | ------------------------------------------------ |
| `NITRO_API_KEY` | Authentication key for the Nitro API             |
| `NITRO_API_ID`  | Your Nitro API identifier                        |
| `NITRO_STAGE`   | Deployment stage (e.g., `production`, `staging`) |

Then register the bus with instrumentation:

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

# Log-trace correlation

When OpenTelemetry is configured with `AddOpenTelemetry().WithLogging()`, your `ILogger` structured log entries automatically include `TraceId` and `SpanId` fields. This means every log line written inside a handler or middleware is automatically correlated to the active span, making it possible to jump from a log entry in your logging backend directly to the corresponding trace.

The Aspire configuration block above already includes `builder.Logging.AddOpenTelemetry(...)`, which enables this behavior. No additional code is needed in your handlers.

For more detail on .NET distributed tracing concepts and how `Activity` maps to spans, see [.NET Distributed Tracing Concepts](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts).

# Next steps

- [Middleware and Pipelines](./middleware-and-pipelines.md) - Understand how `DispatchInstrumentation`, `ReceiveInstrumentation`, and `ConsumerInstrumentation` fit into each pipeline.
- [Reliability](./reliability.md) - Configure fault handling and circuit breakers that work alongside observability.
- [Sagas](./sagas.md) - For long-running workflows that span multiple messages, see Sagas.

> **Runnable example:** [OpenTelemetry](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Observability/OpenTelemetry)
>
> **Full demo:** [Demo.ServiceDefaults](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.ServiceDefaults) shows how to configure OpenTelemetry tracing and metrics for all services in a shared project, including the `"Mocha"` activity source. The [Demo.AppHost](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.AppHost) orchestrates everything with .NET Aspire for end-to-end observability.

---

### Website Content Docs Mocha Quick Start (`website/content/docs/mocha/quick-start.md`)

---
title: "Quick Start"
description: "Get started with Mocha in under five minutes. Install packages, register the message bus, define an event handler, publish your first event, and verify it works."
---

By the end of this guide, you will have an ASP.NET Core app that publishes an `OrderPlaced` event and handles it - all running in-process with the InMemory transport.

Here is what you are building:

<MochaTopologyVisualization data='{"services":[{"host":{"serviceName":"MochaQuickStart","assemblyName":"MochaQuickStart.dll","instanceId":"quickstart-1"},"messageTypes":[{"identity":"msg:OrderPlaced","runtimeType":"OrderPlaced","runtimeTypeFullName":"MochaQuickStart.OrderPlaced","isInterface":false,"isInternal":false}],"consumers":[{"name":"OrderPlacedHandler","identityType":"OrderPlacedHandler","identityTypeFullName":"MochaQuickStart.OrderPlacedHandler"}],"routes":{"inbound":[{"kind":"subscribe","messageTypeIdentity":"msg:OrderPlaced","consumerName":"OrderPlacedHandler","endpoint":{"name":"order-placed-handler","address":"loopback://localhost/q/order-placed-handler","transportName":"InMemory"}}],"outbound":[{"kind":"publish","messageTypeIdentity":"msg:OrderPlaced","endpoint":{"name":"OrderPlaced","address":"loopback://localhost/c/OrderPlaced","transportName":"InMemory"}}]},"sagas":[]}],"transports":[{"identifier":"inmemory","name":"InMemory","schema":"loopback","transportType":"InMemoryTransport","receiveEndpoints":[{"name":"order-placed-handler","kind":"default","address":"loopback://localhost/q/order-placed-handler","source":{"address":"loopback://localhost/q/order-placed-handler"}}],"dispatchEndpoints":[{"name":"OrderPlaced","kind":"default","address":"loopback://localhost/c/OrderPlaced","destination":{"address":"loopback://localhost/c/OrderPlaced"}}],"topology":{"address":"loopback://localhost","entities":[{"kind":"channel","name":"OrderPlaced","address":"loopback://localhost/c/OrderPlaced","flow":"inbound","properties":{"type":"publish"}},{"kind":"queue","name":"order-placed-handler","address":"loopback://localhost/q/order-placed-handler","flow":"outbound","properties":{}}],"links":[{"kind":"subscription","address":"loopback://localhost/sub/OrderPlaced-order-placed-handler","source":"loopback://localhost/c/OrderPlaced","target":"loopback://localhost/q/order-placed-handler","direction":"forward","properties":{}}]}}]}' trace='{"traceId":"quickstart-trace-001","activities":[{"id":"qs-1","parentId":null,"startTime":"2024-06-15T10:30:00.000Z","durationMs":35,"status":"ok","operation":"publish","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","transport":"InMemory"},{"id":"qs-2","parentId":"qs-1","startTime":"2024-06-15T10:30:00.001Z","durationMs":2,"status":"ok","operation":"dispatch","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","endpointName":"OrderPlaced","endpointAddress":"loopback://localhost/c/OrderPlaced","transport":"InMemory"},{"id":"qs-3","parentId":"qs-2","startTime":"2024-06-15T10:30:00.008Z","durationMs":3,"status":"ok","operation":"receive","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","endpointName":"order-placed-handler","endpointAddress":"loopback://localhost/q/order-placed-handler","transport":"InMemory"},{"id":"qs-4","parentId":"qs-3","startTime":"2024-06-15T10:30:00.011Z","durationMs":10,"status":"ok","operation":"consume","messageType":"OrderPlaced","messageTypeIdentity":"msg:OrderPlaced","consumerName":"OrderPlacedHandler"}]}' />

# Create the project

```shell
dotnet new web -n MochaQuickStart
cd MochaQuickStart
```

# Install the packages

You need two packages: the core bus and the InMemory transport.

```shell
dotnet add package Mocha
dotnet add package Mocha.Transport.InMemory
```

The InMemory transport keeps everything in-process - no broker to install, no infrastructure to configure. It is the fastest way to get started.

# Define a message

A message is a plain C# record. Create a file called `OrderPlaced.cs`:

```csharp filename="OrderPlaced.cs"
namespace MochaQuickStart;

public sealed record OrderPlaced(
    Guid OrderId,
    string ProductName,
    decimal Amount);
```

No base class, no marker interface. Any record or class works as a message.

# Create a handler

A handler is a class that implements `IEventHandler<T>`. Create a file called `OrderPlacedHandler.cs`:

```csharp
using Mocha;

namespace MochaQuickStart;

public class OrderPlacedHandler(ILogger<OrderPlacedHandler> logger)
    : IEventHandler<OrderPlaced>
{
    public ValueTask HandleAsync(
        OrderPlaced message,
        CancellationToken cancellationToken)
    {
        logger.LogInformation(
            "Order received: {OrderId} - {ProductName} for {Amount:C}",
            message.OrderId,
            message.ProductName,
            message.Amount);

        return ValueTask.CompletedTask;
    }
}
```

The bus calls `HandleAsync` every time an `OrderPlaced` event is published. The handler receives the deserialized message and a cancellation token.

# Register the bus

Open `Program.cs` and replace its contents:

```csharp filename="Program.cs"
using Mocha;
using Mocha.Transport.InMemory;
using MochaQuickStart;

var builder = WebApplication.CreateBuilder(args);

// Register the message bus, handlers, and transport
builder.Services
    .AddMessageBus()
    .AddMochaQuickStart() // source-generated - discovers all handlers in this assembly
    .AddInMemory();

var app = builder.Build();

// Endpoint that publishes an event
app.MapPost("/orders", async (IMessageBus bus) =>
{
    var orderPlaced = new OrderPlaced(
        OrderId: Guid.NewGuid(),
        ProductName: "Mechanical Keyboard",
        Amount: 149.99m);

    await bus.PublishAsync(orderPlaced, CancellationToken.None);

    return Results.Ok(new { orderPlaced.OrderId, Status = "Published" });
});

app.Run();
```

Each registration line has a single responsibility:

- `AddMessageBus()` - registers the bus runtime and core services into DI.
- `AddMochaQuickStart()` - source-generated method that discovers and registers all handlers in this assembly. Named after the project - to customize the name, see [Handler Registration](./handler-registration.md).
- `AddInMemory()` - adds the InMemory transport; messages stay in-process.

# Publish and verify

Run the app:

```shell
dotnet run
```

Check your console output for the actual URL. ASP.NET Core's default port may differ depending on your SDK version and launch settings. Then, in another terminal, send a POST request using that URL:

```shell
curl -X POST http://localhost:5000/orders
```

You should see JSON from curl:

```json
{ "orderId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "Published" }
```

And in the application console, the handler's log message appears:

```text
info: MochaQuickStart.OrderPlacedHandler[0]
      Order received: a1b2c3d4-e5f6-7890-abcd-ef1234567890 - Mechanical Keyboard for $149.99
```

If you see that log line, it worked.

## What happened?

Your POST request hit the `/orders` endpoint, which called `PublishAsync` on `IMessageBus`. The bus serialized the `OrderPlaced` record and handed it to the InMemory transport. The transport delivered it to the registered receive endpoint, which ran the message through the pipeline and invoked `HandleAsync` on your `OrderPlacedHandler`. The log line you see is proof the full path executed: publisher to bus to transport to handler.

# Next steps

You have a working message bus. Here is where to go next:

- **Understand messages:** [Messages](./messages.md) - learn what a message is, how the envelope wraps it, and naming conventions for events and commands.
- **Learn the three patterns:** [Messaging Patterns](./messaging-patterns.md) - understand when to use pub/sub events, commands, and request/reply.
- **Move to production:** [Transports](./transports/index.md) - switch from InMemory to RabbitMQ for real workloads.

Now that you have a working app, learn how messages work in [Messages](./messages.md).

> **Runnable example:** [Examples/QuickStart](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/QuickStart)
>
> **Full demo:** The [Demo application](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo) shows a complete e-commerce system with Catalog, Billing, and Shipping services communicating through Mocha and orchestrated with .NET Aspire.

---

### Website Content Docs Mocha Reliability (`website/content/docs/mocha/reliability.md`)

---
title: "Reliability"
description: "Configure retries, fault handling, dead-letter routing, the transactional outbox, and the idempotent inbox to build resilient Mocha pipelines."
---

Messaging systems fail. Handlers throw exceptions, brokers go offline, databases lock up, and messages arrive faster than consumers can process them. Mocha's reliability features handle these failures at the infrastructure level so your handler code stays focused on business logic.

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        policy.On<ValidationException>().DeadLetter();
        policy.Default().Retry().ThenRedeliver();
    })
    .AddCircuitBreaker(opts =>
    {
        opts.FailureRatio = 0.5;
        opts.BreakDuration = TimeSpan.FromSeconds(30);
    })
    .AddConcurrencyLimiter(opts => opts.MaxConcurrency = 10)
    .AddEventHandler<OrderPlacedHandler>()
    .AddEntityFramework<AppDbContext>(p =>
    {
        p.UsePostgresOutbox();
        p.UseTransaction();
        p.UsePostgresInbox();
    })
    .AddRabbitMQ();
```

That configuration adds per-exception retry and redelivery policies, circuit breaking, concurrency limiting, transactional outbox, idempotent inbox, and database transaction wrapping - all as middleware in the receive and dispatch pipelines.

# Delivery guarantees

The outbox and inbox change what delivery guarantee your system provides:

| Configuration       | Guarantee                | What it means                                                                                                                                  |
| ------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Without outbox      | At-most-once             | A message may be lost if the broker or handler crashes after receipt but before processing completes.                                          |
| With outbox         | At-least-once            | Every message is persisted before dispatch. Your handlers may be invoked more than once if a crash occurs between dispatch and acknowledgment. |
| With outbox + inbox | Effectively exactly-once | The outbox guarantees every message is delivered. The inbox deduplicates on the receiving side, so each message is processed exactly once.     |

At-least-once delivery is the right default for most production systems. Adding the inbox on the consumer side upgrades the guarantee to effectively exactly-once processing - the outbox ensures delivery and the inbox ensures your handler runs only once per message. If you use the outbox without the inbox, design handlers to be idempotent.

# The receive pipeline and failure flow

Mocha processes every inbound message through a compiled receive pipeline. The default middleware runs in this order:

```text
TransportCircuitBreaker
  -> ConcurrencyLimiter
    -> Instrumentation
      -> DeadLetter
        -> Fault
          -> Redelivery          <- schedules for later if retries exhausted
            -> CircuitBreaker
              -> Expiry
                -> MessageTypeSelection
                  -> Routing
                    -> Consumer pipeline
                      -> Retry   <- immediate in-process retries
                        -> Transaction middleware (BEGIN)
                          -> Inbox (claim inside transaction)
                            -> Your handler
                        -> Transaction middleware (COMMIT/ROLLBACK)
```

Each middleware can intercept failures from downstream, transform them, or short-circuit the pipeline. The reliability middlewares - dead-letter, fault, circuit breaker, expiry, inbox, and concurrency limiter - are all enabled by default with sensible defaults. You tune them when the defaults do not match your workload.

# Handle faults

When your handler throws an exception, Mocha's fault middleware catches it and takes one of two actions depending on the messaging pattern:

**Request/reply flows:** The fault middleware sends a `NotAcknowledgedEvent` back to the caller's response address. This gives the requester an explicit failure signal instead of a timeout.

**Pub/sub and send flows:** The fault middleware forwards the original message envelope to the endpoint's error endpoint with fault metadata in the headers. The headers include the exception type, message, stack trace, and timestamp:

| Header key             | Value                               |
| ---------------------- | ----------------------------------- |
| `fault-exception-type` | Fully qualified exception type name |
| `fault-message`        | Exception message string            |
| `fault-stack-trace`    | Stack trace of the exception        |
| `fault-timestamp`      | ISO 8601 timestamp of the fault     |

The fault middleware marks the message as consumed after handling, which prevents the dead-letter middleware from re-forwarding it.

The fault handler is implemented as the `Fault` middleware in the receive pipeline. See [Middleware and Pipelines](./middleware-and-pipelines.md) for positioning and customization.

## Verify fault behavior

Throw an exception in a handler and inspect the error queue. With RabbitMQ, the error endpoint writes faulted messages to an `_error` queue by convention.

```csharp
public class OrderPlacedHandler : IEventHandler<OrderPlacedEvent>
{
    public ValueTask HandleAsync(
        OrderPlacedEvent message,
        CancellationToken cancellationToken)
    {
        throw new InvalidOperationException("Simulated failure");
    }
}
```

Publish an `OrderPlacedEvent` and check your RabbitMQ management console. The message appears in the error queue with fault headers attached.

# Route unhandled messages to the dead-letter endpoint

The dead-letter middleware is the pipeline's safety net. It runs early in the pipeline (before the fault middleware) and catches any message that reaches the end of the pipeline without being consumed. This covers scenarios the fault middleware does not: messages with no matching consumer, messages that fail during routing, or messages that fall through all middleware without being handled.

When a message is not consumed, the dead-letter middleware forwards the original envelope to the endpoint's error endpoint, preserving all headers and payload for later inspection.

```text
info: Mocha.Middlewares.ReceiveDeadLetterMiddleware[0]
      An exception occurred while processing the message.
      The message will be moved to the error endpoint.
```

The dead-letter middleware logs at `Critical` level when it catches an exception, then forwards the message. If no error endpoint is configured for the receive endpoint, the dead-letter middleware is not activated.

See [Dead Letter Channel](https://www.enterpriseintegrationpatterns.com/patterns/messaging/DeadLetterChannel.html) for the canonical description of this pattern.

# Expire stale messages

Messages can carry a `DeliverBy` timestamp that marks when they become stale. The expiry middleware checks this timestamp before any deserialization or handler work runs. If the current time exceeds `DeliverBy`, the message is silently dropped and marked as consumed - no exception, no dead-letter, no handler invocation.

## Set message expiry on publish

```csharp
await bus.PublishAsync(
    new PriceQuoteEvent { Ticker = "MSFT", Price = 425.30m },
    new PublishOptions
    {
        ExpirationTime = DateTimeOffset.UtcNow.AddMinutes(5)
    },
    cancellationToken);
```

The `ExpirationTime` on `PublishOptions` maps to the `DeliverBy` envelope field. When the message sits in a queue longer than five minutes, the expiry middleware drops it on arrival.

## Set message expiry on send

```csharp
await bus.SendAsync(
    new ProcessPaymentCommand { OrderId = orderId, Amount = 99.99m },
    new SendOptions
    {
        ExpirationTime = DateTimeOffset.UtcNow.AddSeconds(30)
    },
    cancellationToken);
```

For time-sensitive commands, a short expiry prevents stale operations from executing after their validity window.

# Retry failed messages

When a handler throws a transient exception - a database timeout, an HTTP 503, temporary lock contention - retrying the same operation a few hundred milliseconds later often succeeds. The retry middleware re-runs the handler in-process without releasing the concurrency slot or leaving the consumer pipeline.

Retry lives in the **consumer pipeline**, not the receive pipeline. This matters for two reasons:

1. **Only handler failures are retried.** Deserialization errors, unknown message types, and routing failures are almost always permanent. Retrying them wastes time.
2. **Multi-consumer correctness.** When one endpoint routes a message to multiple consumers, only the failing consumer retries. The others are unaffected.

## Add retry with defaults

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

The parameterless `AddResilience()` registers a catch-all `Default()` rule with both retry and redelivery enabled. Retry defaults to 3 attempts, 200 ms base delay, exponential backoff, jitter enabled, and a 30-second maximum delay. These defaults handle the majority of transient failures without tuning.

## Customize retry behavior

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        policy.Default()
            .Retry(5, TimeSpan.FromSeconds(1), RetryBackoffType.Exponential)
            .ThenRedeliver();
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

## Use explicit retry intervals

When you need precise control over each delay, pass an array of intervals. The array length determines the number of retries.

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        policy.Default().Retry(
        [
            TimeSpan.FromMilliseconds(100),
            TimeSpan.FromMilliseconds(500),
            TimeSpan.FromSeconds(2)
        ]).ThenRedeliver();
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

## Handle different exceptions differently

Not every exception should be retried. Validation errors, authorization failures, and other permanent errors waste retry budget. Use `AddResilience` to define per-exception handling strategies.

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        // Never retry validation failures - route to error endpoint
        policy.On<ValidationException>().DeadLetter();

        // Never retry non-transient database errors
        policy.On<DbException>(ex => !ex.IsTransient).DeadLetter();

        // Transient database errors get more retries
        policy.On<DbException>(ex => ex.IsTransient)
            .Retry(5, TimeSpan.FromMilliseconds(200))
            .ThenRedeliver();

        // Everything else: retry then redeliver
        policy.Default().Retry().ThenRedeliver();
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

Exception matching respects inheritance. `On<DbException>()` also matches `NpgsqlException` and any other `DbException` subclass. When multiple rules match, the most specific type wins - the same precedence as C# `catch` blocks.

For the full API including predicates, conditional policies, and escalation chains, see [Exception Policies](./exception-policies.md).

## Override retry per consumer

The bus-level exception policy applies to all consumers. Override it for specific consumers that need different behavior. See [Handlers and Consumers](./handlers-and-consumers.md) for the full consumer configuration API.

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience() // bus-level: default retry + redelivery for all consumers
    .AddEventHandler<OrderHandler>(consumer =>
    {
        // This consumer: 10 retries with longer delay, then redeliver
        consumer.AddResilience(policy =>
        {
            policy.Default()
                .Retry(10, TimeSpan.FromSeconds(2))
                .ThenRedeliver();
        });
    })
    .AddEventHandler<NotificationHandler>(consumer =>
    {
        // This consumer: skip retry, redeliver only
        consumer.AddResilience(policy =>
        {
            policy.Default().Redeliver();
        });
    })
    .AddRabbitMQ();
```

Consumer-level policies replace the bus-level policies entirely for that consumer - they are not merged. If you need a bus-level rule to also apply at the consumer level, include it in the consumer-level configuration.

## Retry defaults reference

| Setting   | Default value |
| --------- | ------------- |
| Attempts  | 3             |
| Delay     | 200 ms        |
| Backoff   | Exponential   |
| Jitter    | Enabled       |
| Max delay | 30 seconds    |

### RetryBackoffType values

| Value         | Formula                   | Example (Delay = 200 ms) |
| ------------- | ------------------------- | ------------------------ |
| `Constant`    | `Delay`                   | 200 ms, 200 ms, 200 ms   |
| `Linear`      | `Delay * attempt`         | 200 ms, 400 ms, 600 ms   |
| `Exponential` | `Delay * 2^(attempt - 1)` | 200 ms, 400 ms, 800 ms   |

# Redeliver failed messages

Retry handles transient blips that resolve in milliseconds. Some failures take longer - a downstream service is deploying, a database is recovering from a failover, an external API is rate-limiting. For these, you need **redelivery**: reschedule the message for later delivery through the transport.

Redelivery lives in the **receive pipeline**, not the consumer pipeline. This matters for two reasons:

1. **Single decision point.** When an endpoint has multiple consumers, you want one redelivery decision per message, not one per consumer.
2. **Releases the concurrency slot.** Retry holds the slot while waiting. Redelivery returns the message to the transport and frees the slot for other messages.

Redelivery uses Mocha's [scheduling infrastructure](./scheduling.md) to schedule the message for future delivery. The message re-enters the full receive pipeline when it arrives - fresh routing, fresh consumer invocations, fresh retry attempts.

```text
Receive pipeline:

  Fault
    -> Redelivery
      -> CircuitBreaker
        -> ... (rest of pipeline)

All retries exhausted -> exception propagates to Redelivery
  -> redelivery attempts remaining -> schedule for later delivery
  -> redelivery exhausted -> exception propagates to Fault -> error endpoint
```

## Add redelivery with defaults

Redelivery is included when you call `AddResilience()` without arguments or when you chain `.ThenRedeliver()` in an escalation chain.

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience() // Default() rule enables both retry and redelivery
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

The default redelivery schedule is three attempts at 5 minutes, 15 minutes, and 30 minutes with jitter enabled and a 1-hour maximum delay.

## Use custom redelivery intervals

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        policy.Default().Retry().ThenRedeliver(
        [
            TimeSpan.FromSeconds(30),
            TimeSpan.FromMinutes(5),
            TimeSpan.FromMinutes(30)
        ]);
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

The array length determines the number of redelivery attempts. Each element is the delay before that attempt.

## Use calculated redelivery delays

Instead of explicit intervals, configure a number of attempts and a base delay:

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        policy.Default()
            .Retry()
            .ThenRedeliver(5, TimeSpan.FromMinutes(2));
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

## Redeliver without retry

To skip retry entirely and go straight to redelivery, use `.Redeliver()` directly:

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        policy.Default().Redeliver();
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

When `.Redeliver()` is called without `.Retry()` first, retry is disabled for that rule. The handler failure goes straight to redelivery scheduling.

## Override redelivery per endpoint

Override exception policies at the transport level for more granular control. Transport-level policies replace the bus-level policies entirely for all endpoints on that transport.

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience() // bus-level default
    .AddRabbitMQ(transport =>
    {
        transport.AddResilience(policy =>
        {
            // Override for this transport: longer redelivery intervals
            policy.Default().Retry().ThenRedeliver(
            [
                TimeSpan.FromMinutes(10),
                TimeSpan.FromMinutes(30),
                TimeSpan.FromHours(1)
            ]);
        });
    });
```

Disable redelivery for a specific transport by configuring retry-only:

```csharp
transport.AddResilience(policy =>
{
    policy.Default().Retry();
});
```

## Redelivery defaults reference

| Setting   | Default value         |
| --------- | --------------------- |
| Intervals | 5 min, 15 min, 30 min |
| Jitter    | Enabled               |
| Max delay | 1 hour                |

# Combine retry and redelivery

The two tiers compose naturally through escalation chains. When both are configured, a message goes through immediate retry first. If all retries are exhausted, the exception propagates up from the consumer pipeline to the receive pipeline, where redelivery catches it and schedules the message for later delivery. On the next delivery round, the full retry cycle runs again.

```text
Message arrives
  |
  +- Consumer pipeline: Retry
  |    +- Attempt 1 -> handler throws -> retry
  |    +- Attempt 2 -> handler throws -> retry
  |    +- Attempt 3 -> handler throws -> retry
  |    +- Attempt 4 -> handler throws -> retries exhausted, exception propagates
  |
  +- Receive pipeline: Redelivery
  |    +- Schedule message for delivery in 5 minutes
  |
  +- ... 5 minutes later, message re-enters pipeline ...
  |
  +- Consumer pipeline: Retry
  |    +- Attempt 1 -> handler throws -> retry
  |    +- ...
  |    +- Attempt 4 -> retries exhausted, exception propagates
  |
  +- Receive pipeline: Redelivery
  |    +- Schedule message for delivery in 15 minutes
  |
  +- ... continues until redelivery attempts exhausted ...
  |
  +- Fault middleware -> error endpoint (dead letter)
```

The total number of handler invocations before a message reaches the error endpoint:

```text
Total attempts = (retry attempts + 1) x (redelivery attempts + 1)
```

With the defaults (3 retries, 3 redeliveries): `(3 + 1) x (3 + 1) = 16` total handler invocations.

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        policy.Default()
            .Retry(5, TimeSpan.FromSeconds(1), RetryBackoffType.Exponential)
            .ThenRedeliver(
            [
                TimeSpan.FromMinutes(5),
                TimeSpan.FromMinutes(15),
                TimeSpan.FromMinutes(30)
            ]);
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

With this configuration: `(5 + 1) x (3 + 1) = 24` total handler invocations before dead-letter.

> [!NOTE]
> Request/reply messages skip redelivery. Redelivery does not apply to request/reply messages. The caller is waiting synchronously for a response - scheduling the message for delivery minutes later would cause a timeout. When a request/reply handler fails after retry exhaustion, the exception propagates directly to the fault middleware, which sends a `NotAcknowledgedEvent` back to the caller. Immediate retry still applies to request/reply handlers.

# Inspect retry state in handlers

Handlers can access the current retry state through the context features. This is useful for graceful degradation - trying a primary path on the first attempt and falling back to an alternative on subsequent retries.

```csharp
public class PaymentConsumer(ILogger<PaymentConsumer> logger) : IConsumer<ProcessPayment>
{
    public async ValueTask ConsumeAsync(IConsumeContext<ProcessPayment> context)
    {
        var state = context.Features.Get<RetryFeature>();

        if (state is { ImmediateRetryCount: > 2 })
        {
            logger.LogWarning(
                "Primary gateway failed after {Retries} retries, using fallback",
                state.ImmediateRetryCount);
            await ProcessViaFallbackGatewayAsync(context.Message);
            return;
        }

        if (state is { DelayedRetryCount: > 0 })
        {
            logger.LogWarning(
                "Processing after {Redeliveries} redeliveries",
                state.DelayedRetryCount);
        }

        await ProcessPaymentAsync(context.Message);
    }
}
```

`RetryFeature` is `null` when `AddResilience()` is not configured. When present, it exposes two properties:

| Property              | Type  | Description                                                                                |
| --------------------- | ----- | ------------------------------------------------------------------------------------------ |
| `ImmediateRetryCount` | `int` | Number of immediate retries so far in this delivery round. `0` on the initial attempt.     |
| `DelayedRetryCount`   | `int` | Number of redelivery rounds completed. Read from the `delayed-retry-count` message header. |

## Retry and sagas

[Sagas](./sagas.md) are consumers, so retry and redelivery apply automatically - no special configuration needed. Each saga handler invocation runs inside a saga transaction. If the handler throws, the transaction is never committed and all state changes are discarded. On the next retry attempt, the saga state is loaded fresh from the store.

This means retry is safe for sagas by default. You do not need to worry about partial state mutations leaking between retry attempts.

Redelivery is also safe. When a redelivered message arrives, a new transaction starts and the saga state is loaded at whatever state the last successful commit left it.

# Troubleshoot retry and redelivery

## "My handler runs more times than expected"

Check both retry and redelivery. With both configured, the total handler invocations are `(retry attempts + 1) x (redelivery attempts + 1)`. Three retries with three redeliveries means 16 total invocations, not 6. Use the [formula above](#combine-retry-and-redelivery) to calculate the exact count.

## "Retry does not work for my consumer"

`AddResilience()` must be called at the bus level (or on the specific consumer) to register the retry middleware in the consumer pipeline. Adding a consumer-level policy without a bus-level or consumer-level `AddResilience()` call has no effect - the middleware is not in the pipeline.

## "Redelivery fails at startup"

Redelivery uses Mocha's [scheduling infrastructure](./scheduling.md). If your transport does not support scheduling or the scheduling store is not configured, redelivery cannot schedule messages for later delivery. Check that your transport is configured with scheduling support.

## "Validation exceptions are being retried"

Use `AddResilience` to route permanent failures directly to the error endpoint:

```csharp
builder.Services
    .AddMessageBus()
    .AddResilience(policy =>
    {
        policy.On<ValidationException>().DeadLetter();
        policy.Default().Retry().ThenRedeliver();
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

See [Exception Policies](./exception-policies.md) for the full per-exception configuration API, including predicates, terminal actions, and escalation chains.

## "Request/reply messages are not redelivered"

This is by design. Redelivery skips request/reply messages because the caller is waiting for a synchronous response. Immediate retry still applies. See the [note above](#combine-retry-and-redelivery) for details.

# Limit concurrency

The concurrency limiter middleware restricts how many messages a receive endpoint processes in parallel.

```csharp
builder.Services
    .AddMessageBus()
    .AddConcurrencyLimiter(opts => opts.MaxConcurrency = 10)
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

The default `MaxConcurrency` is `Environment.ProcessorCount * 2`. Set it lower when your handlers access shared resources with limited capacity (database connection pools, external APIs with rate limits), or higher when handlers are I/O-bound and the resource can handle more parallel work.

The concurrency limiter is enabled by default. To disable it for a specific scope, set `Enabled` to `false`:

```csharp
builder.Services
    .AddMessageBus()
    .AddConcurrencyLimiter(opts => opts.Enabled = false)
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

## Configuration scoping

Both the concurrency limiter and circuit breaker resolve configuration using scope precedence: **endpoint > transport > bus**. The most specific scope wins.

Configure at the bus level to set a global default:

```csharp
builder.Services
    .AddMessageBus()
    .AddConcurrencyLimiter(opts => opts.MaxConcurrency = 10)
    .AddRabbitMQ();
```

Override at the transport or endpoint level for more granular control. Transport and endpoint descriptors accept the same `.AddConcurrencyLimiter()` extension:

```csharp
builder.Services
    .AddMessageBus()
    .AddConcurrencyLimiter(opts => opts.MaxConcurrency = 20) // bus-level default
    .AddRabbitMQ(transport =>
    {
        transport.AddConcurrencyLimiter(opts => opts.MaxConcurrency = 5); // override for RabbitMQ
    });
```

# Configure the circuit breaker

The circuit breaker middleware stops processing messages when the failure rate exceeds a threshold. It uses [Polly](https://github.com/App-vNext/Polly) internally and follows the standard circuit breaker pattern: **closed** (normal), **open** (rejecting), **half-open** (testing recovery).

The circuit breaker is implemented as the `CircuitBreaker` middleware in the receive pipeline. See [Middleware and Pipelines](./middleware-and-pipelines.md) for positioning and customization.

```csharp
builder.Services
    .AddMessageBus()
    .AddCircuitBreaker(opts =>
    {
        opts.FailureRatio = 0.5;
        opts.MinimumThroughput = 10;
        opts.SamplingDuration = TimeSpan.FromSeconds(10);
        opts.BreakDuration = TimeSpan.FromSeconds(30);
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

When the circuit opens, the middleware pauses message processing for `BreakDuration` before allowing a test message through. If the test succeeds, the circuit closes. If it fails, the circuit stays open for another break period.

## How the circuit breaker evaluates failures

1. During the `SamplingDuration` window, the breaker counts successes and failures.
2. After `MinimumThroughput` messages have been processed, it evaluates the `FailureRatio`.
3. If the ratio of failures to total messages exceeds `FailureRatio`, the circuit opens.
4. After `BreakDuration`, the circuit transitions to half-open and allows one message through.
5. If that message succeeds, the circuit closes. If it fails, the circuit reopens.

## Two circuit breaker scopes

Mocha includes two separate circuit breaker middlewares in the default pipeline:

**Receive-level circuit breaker** (`CircuitBreaker`): Runs inside the pipeline after the dead-letter and fault middlewares. It monitors handler-level failures. Configure it with `.AddCircuitBreaker()` on the host builder.

**Transport-level circuit breaker** (`TransportCircuitBreaker`): Runs at the very start of the pipeline, before the concurrency limiter. It monitors transport-level connectivity failures with a lower default failure ratio (10%). Configure it through the transport's options.

The transport breaker protects against cascading failures when the broker itself is degraded. The receive-level breaker protects against application-level handler failures. Both use Polly's `CircuitBreakerStrategyOptions` internally.

# Guarantee delivery with the transactional outbox

The transactional outbox solves the dual-write problem: when your handler writes to a database and publishes a message, either operation can fail independently.

- If the database commits but the publish fails, the event is lost - downstream consumers never see it.
- If the publish succeeds but the database rolls back, the event describes state that never existed.

The outbox writes messages to the same database transaction as your business data. A background processor picks up persisted messages and dispatches them to the transport, providing at-least-once delivery guarantees.

```mermaid
sequenceDiagram
    participant H as Handler
    participant DB as Outbox Table
    participant P as Outbox Processor
    participant T as Transport

    H->>DB: Write business data + outbox message<br/>(same transaction)
    DB-->>H: Transaction committed
    P->>DB: Read pending outbox messages
    P->>T: Dispatch message to transport
    T-->>P: Acknowledged
    P->>DB: Delete outbox message
```

See [Transactional Outbox](https://microservices.io/patterns/data/transactional-outbox.html) and [Guaranteed Delivery](https://www.enterpriseintegrationpatterns.com/patterns/messaging/GuaranteedMessaging.html) for the canonical pattern descriptions.

## Set up the Postgres outbox

**1. Add the NuGet packages.**

```shell
dotnet add package Mocha.EntityFrameworkCore
dotnet add package Mocha.EntityFrameworkCore.Postgres
```

**2. Add the `OutboxMessage` entity to your DbContext.**

```csharp
using Microsoft.EntityFrameworkCore;
using Mocha.Outbox;

public class AppDbContext : DbContext
{
    public DbSet<OutboxMessage> OutboxMessages => Set<OutboxMessage>();

    // Your existing DbSets
    public DbSet<Order> Orders => Set<Order>();
}
```

`OutboxMessage` has four columns: `Id` (Guid), `Envelope` (JsonDocument), `TimesSent` (int), and `CreatedAt` (DateTime). EF Core discovers the table name, schema, and column mappings from your model automatically.

**3. Register the outbox and transaction middleware.**

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

| Call                             | Purpose                                                                                            |
| -------------------------------- | -------------------------------------------------------------------------------------------------- |
| `AddEntityFramework<TContext>()` | Registers your DbContext with the bus for persistence features.                                    |
| `UsePostgresOutbox()`            | Registers the Postgres outbox processor, background worker, and `IMessageOutbox`.                  |
| `UseTransaction()`               | Wraps each consumer invocation in a database transaction (commit on success, rollback on failure). |

**4. Publish inside a transaction.**

With the outbox enabled, calls to `PublishAsync`, `SendAsync`, and `ReplyAsync` inside a handler persist the message envelope to the outbox table within the same database transaction. The outbox dispatch middleware intercepts these calls and writes to `IMessageOutbox` instead of the transport.

```csharp
public class OrderPlacedHandler(AppDbContext db, IMessageBus bus)
    : IEventHandler<OrderPlacedEvent>
{
    public async ValueTask HandleAsync(
        OrderPlacedEvent message,
        CancellationToken cancellationToken)
    {
        var invoice = new Invoice { OrderId = message.OrderId, Amount = message.Amount };
        db.Invoices.Add(invoice);

        // This write goes to the outbox table, not directly to the transport
        await bus.PublishAsync(
            new InvoiceCreatedEvent { InvoiceId = invoice.Id },
            cancellationToken);

        await db.SaveChangesAsync(cancellationToken);
        // Transaction commits both the invoice AND the outbox message atomically
    }
}
```

After the transaction commits, the outbox processor detects the new message (via EF Core interceptors that signal on save and transaction commit) and dispatches it to the transport.

## Transaction requirements

By default, the outbox requires a transaction for `PublishAsync` and `SendAsync`. Without an open database transaction on the DbContext or an ambient `TransactionScope`, these calls throw an `InvalidOperationException` and no message is written. Replies and faults are written without a transaction.

Direct bus publishes use the first DbContext registered with `UsePostgresOutbox()`. A transaction on another DbContext does not satisfy this requirement.

Inside a transaction, the message is written to the outbox table as part of that transaction. Consumers get a transaction from `UseTransaction()`, and mediator handlers from `UseEntityFrameworkTransactions<TContext>()`. In other code, open one on the DbContext:

```csharp
await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken);

db.Orders.Add(order);
await bus.PublishAsync(new OrderPlacedEvent { OrderId = order.Id }, cancellationToken);
await db.SaveChangesAsync(cancellationToken);

await transaction.CommitAsync(cancellationToken);
```

To publish outside a transaction, set `RequireTransaction` to `false`:

```csharp
builder.Services
    .AddMessageBus()
    .AddEntityFramework<AppDbContext>(p =>
    {
        p.UsePostgresOutbox(o => o.RequireTransaction = false);
    })
    .AddRabbitMQ();
```

With `RequireTransaction` set to `false`, a message published outside a transaction is written to the outbox table immediately and dispatched right away, independent of any later `SaveChangesAsync`. Inside a transaction, it is still written as part of that transaction.

`PostgresOutboxOptions` also supports named options configuration. The options name is the DbContext type's full name:

```csharp
using Mocha.Outbox;

builder.Services.Configure<PostgresOutboxOptions>(
    typeof(AppDbContext).FullName!,
    options => options.RequireTransaction = false);
```

> [!NOTE]
> The outbox guarantees at-least-once delivery. Your handlers may be invoked more than once for the same message if the outbox dispatches successfully but the transport acknowledgment is lost before the message is deleted from the outbox table. You can handle this in two ways: design handlers to be idempotent manually (see the [Idempotent Consumer](https://microservices.io/patterns/communication-style/idempotent-consumer.html) pattern), or enable the [inbox](#deduplicate-messages-with-the-transactional-inbox) on the receiving side to let Mocha deduplicate automatically.

## Use execution strategy resilience

For transient database failures such as connection drops or deadlocks, add execution strategy wrapping:

```csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedHandler>()
    .AddEntityFramework<AppDbContext>(p =>
    {
        p.UsePostgresOutbox();
        p.UseResilience(); // Wraps consumer execution with the EF Core execution strategy
        p.UseTransaction();
    })
    .AddRabbitMQ();
```

`UseResilience()` wraps the consumer middleware pipeline in the DbContext's configured execution strategy, enabling automatic retries for transient database errors.

## How the outbox processor works

The outbox processor is a background hosted service (`IHostedService`). When the EF Core interceptors detect a `SaveChanges` or transaction commit, they signal the processor through `IOutboxSignal`. Inside an ambient `TransactionScope`, the signal is raised when the ambient transaction completes. The processor reads pending `OutboxMessage` rows, deserializes the envelope, and dispatches each message through the bus's dispatch pipeline.

The `TimesSent` column tracks dispatch attempts. If dispatch fails, the processor retries on the next signal. Messages are deleted from the outbox table after successful dispatch.

## Skip the outbox for specific dispatches

Some messages - like internal system events or replies that do not need durability - can bypass the outbox. The outbox middleware checks for an `OutboxMiddlewareFeature` on the dispatch context. Messages with `SkipOutbox = true` pass straight through to the transport.

The outbox middleware also only intercepts messages of kind `Publish`, `Send`, `Reply`, or `Fault`. Other message kinds pass through without outbox persistence.

# Deduplicate messages with the transactional inbox

The transactional outbox guarantees at-least-once delivery. The transactional inbox completes the picture: it provides exactly-once processing by deduplicating messages on the receiving side.

When a transport redelivers a message - because of a broker retry, a network hiccup, or an outbox re-dispatch - the inbox detects the duplicate `MessageId` and silently skips it. Your handler never runs twice for the same message.

Deduplication is scoped per consumer type: when a message is routed to multiple handlers, each handler independently claims and processes the message. The inbox uses a composite key of `(MessageId, ConsumerType)` so that handler A and handler B each process the same message exactly once, even though they share the same inbox table.

```mermaid
sequenceDiagram
    participant T as Transport
    participant TX as Transaction Middleware
    participant I as Inbox Middleware
    participant DB as Inbox Table
    participant H as Handler

    T->>TX: Deliver message (MessageId: abc-123)
    TX->>TX: BEGIN TRANSACTION
    TX->>I: Pass to inbox
    I->>DB: TryClaimAsync("abc-123", "OrderPlacedHandler") [inside transaction]
    DB-->>I: true (claimed)
    I->>H: Process message
    H->>DB: Write business data [same transaction]
    H-->>I: Handler completed
    I-->>TX: Return
    TX->>TX: COMMIT (inbox claim + business data atomic)
    Note over T,DB: Later, transport redelivers the same message
    T->>TX: Deliver message (MessageId: abc-123)
    TX->>TX: BEGIN TRANSACTION
    TX->>I: Pass to inbox
    I->>DB: TryClaimAsync("abc-123", "OrderPlacedHandler") [inside transaction]
    DB-->>I: false (already claimed)
    I-->>TX: Skip (message not processed again)
    TX->>TX: COMMIT (no-op)
```

The inbox middleware runs in the **consumer pipeline**, after the transaction middleware. This means the inbox claim INSERT participates in the same database transaction as the handler's business data. Both commit or rollback atomically: if the process crashes between the claim and the commit, the claim is rolled back and the message can be safely redelivered.

Messages without a `MessageId` pass through the inbox without deduplication - there is no identifier to check against.

See [Idempotent Consumer](https://microservices.io/patterns/communication-style/idempotent-consumer.html) for the canonical description of this pattern.

## Set up the Postgres inbox

**1. Add the NuGet packages.**

```shell
dotnet add package Mocha.EntityFrameworkCore
dotnet add package Mocha.EntityFrameworkCore.Postgres
```

These are the same packages used by the outbox. If you already have them installed for the outbox, skip this step.

**2. Add the `InboxMessage` entity to your DbContext.**

```csharp
using Microsoft.EntityFrameworkCore;
using Mocha.Inbox;

public class AppDbContext : DbContext
{
    public DbSet<InboxMessage> InboxMessages => Set<InboxMessage>();

    // Your existing DbSets
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.AddPostgresInbox();
    }
}
```

`InboxMessage` has four columns: `MessageId` (string), `ConsumerType` (string, the handler type name), `MessageType` (string, nullable, for diagnostics), and `ProcessedAt` (DateTime, defaults to `NOW()`). The primary key is the composite `(MessageId, ConsumerType)`, enabling per-handler deduplication when a message is routed to multiple consumers.

**3. Register the inbox middleware.**

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

| Call                             | Purpose                                                                                                              |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `AddEntityFramework<TContext>()` | Registers your DbContext with the bus for persistence features.                                                      |
| `UsePostgresInbox()`             | Registers the Postgres inbox, background cleanup worker, and `IMessageInbox`. Inserts the inbox consumer middleware. |
| `UseTransaction()`               | Wraps each consumer invocation in a database transaction (commit on success, rollback on failure).                   |

**4. Combine outbox and inbox for full exactly-once processing.**

When you use both together, the outbox guarantees delivery and the inbox guarantees deduplication:

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

With this configuration, the inbox record and your business data are committed in the same database transaction. If the transaction rolls back, the inbox entry is not persisted and the message can be reprocessed on the next delivery attempt.

## Configure inbox retention

The inbox stores a record for every processed message. A background worker (`MessageBusInboxWorker`) periodically cleans up old records to prevent unbounded table growth.

```csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedHandler>()
    .AddEntityFramework<AppDbContext>(p =>
    {
        p.UseTransaction();
        p.UsePostgresInbox(opts =>
        {
            opts.RetentionPeriod = TimeSpan.FromDays(14);
            opts.CleanupInterval = TimeSpan.FromMinutes(30);
        });
    })
    .AddRabbitMQ();
```

| Option            | Default | Description                                                                                              |
| ----------------- | ------- | -------------------------------------------------------------------------------------------------------- |
| `RetentionPeriod` | 7 days  | How long processed message records are kept. Messages older than this are deleted by the cleanup worker. |
| `CleanupInterval` | 1 hour  | How often the background worker runs the cleanup sweep.                                                  |

Set `RetentionPeriod` long enough to cover the maximum redelivery window of your transport. If your transport can redeliver messages up to 3 days after initial delivery, a 7-day retention period provides a comfortable safety margin.

The cleanup worker deletes expired rows in batches to avoid long-running locks on the inbox table.

## Skip the inbox for specific messages

Some messages do not need deduplication - internal system events, heartbeats, or messages from transports that guarantee exactly-once delivery natively. The inbox middleware checks for an `InboxMiddlewareFeature` on the consume context. Messages with `SkipInbox = true` pass straight through without an inbox lookup.

Set the feature from a custom consumer middleware that runs before the inbox:

```csharp
builder.Services
    .AddMessageBus(bus =>
    {
        bus.UseConsume(
            new ConsumerMiddlewareConfiguration(
                static (_, next) => ctx =>
                {
                    var feature = ctx.Features.GetOrSet<InboxMiddlewareFeature>();
                    feature.SkipInbox = true;
                    return next(ctx);
                },
                "SkipInboxCheck"),
            before: "Inbox");
    })
    .AddEventHandler<OrderPlacedHandler>()
    .AddEntityFramework<AppDbContext>(p =>
    {
        p.UseTransaction();
        p.UsePostgresInbox();
    })
    .AddRabbitMQ();
```

`#!csharp UseConsume(..., before: "Inbox")` inserts your middleware immediately before the inbox middleware in the consumer pipeline. The `InboxMiddlewareFeature` is a pooled feature that resets automatically between messages.

## How the inbox cleanup worker works

The inbox cleanup worker is a background hosted service (`IHostedService`). It runs in a continuous loop: wait for `CleanupInterval`, then delete all inbox records where `ProcessedAt` is older than `RetentionPeriod`. Deletions happen in batches to minimize lock contention. The worker logs at `Information` level when records are deleted and at `Error` level if cleanup fails.

# Next steps

Your messaging pipeline now handles exceptions per-type with retry and redelivery policies, limits concurrency, breaks circuits on repeated failures, guarantees delivery through the outbox, and deduplicates messages through the inbox. To monitor your messaging system, see [Observability](./observability.md).

- [**Exception Policies**](./exception-policies.md) - Configure per-exception handling with composable retry, redelivery, and terminal actions.
- [**Handlers and Consumers**](./handlers-and-consumers.md) - Configure per-consumer exception policy overrides and understand handler exception behavior.
- [**Scheduling**](./scheduling.md) - Configure the scheduling infrastructure that redelivery uses for delayed message delivery.
- [**Middleware and Pipelines**](./middleware-and-pipelines.md) - Write custom middleware, control pipeline ordering, and understand the three pipeline stages.
- [**Sagas**](./sagas.md) - Coordinate multi-step workflows with state machine sagas that use compensation when steps fail.
- [**Observability**](./observability.md) - Trace message flows across services and monitor pipeline health with OpenTelemetry.

> **Runnable examples:** [OutboxInbox](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Reliability/OutboxInbox), [CircuitBreaker](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Reliability/CircuitBreaker)
>
> **Full demo:** All three Demo services ([Catalog](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Catalog), [Billing](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Billing), [Shipping](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Shipping)) use the PostgreSQL transactional outbox and inbox with `UseTransaction()` and `UseResilience()` for reliable, exactly-once message processing.

---

### Website Content Docs Mocha Routing And Endpoints (`website/content/docs/mocha/routing-and-endpoints.md`)

---
title: "Routing and Endpoints"
description: "Understand how Mocha discovers endpoints, builds topology, and routes messages using naming conventions - and how to override any of it."
---

An endpoint is the combination of a transport address (a queue or exchange) and a pipeline that processes messages. Mocha distinguishes between receive endpoints (which consume) and dispatch endpoints (which produce). Every handler you register becomes a receive endpoint; every message you publish or send is dispatched through a dispatch endpoint. By default, Mocha creates endpoints automatically from your handler and message types using naming conventions. Most applications never touch routing configuration directly - but you can configure the topology yourself completely when the defaults don't fit.

This page explains what those conventions do, how to verify they produce the topology you expect, and how to override them when the defaults don't fit.

# The endpoint model

When you register handlers and pick a transport, Mocha wires up both sides of the messaging connection automatically:

```csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedHandler>()
    .AddRequestHandler<GetOrderStatusHandler>()
    .AddRabbitMQ();
```

<MochaTopologyVisualization data='{"services":[{"host":{"serviceName":"MyService","assemblyName":"MyService.dll","instanceId":"my-svc-1"},"messageTypes":[{"identity":"msg:OrderPlacedEvent","runtimeType":"OrderPlacedEvent","runtimeTypeFullName":"MyApp.Messages.OrderPlacedEvent","isInterface":false,"isInternal":false},{"identity":"msg:GetOrderStatusRequest","runtimeType":"GetOrderStatusRequest","runtimeTypeFullName":"MyApp.Messages.GetOrderStatusRequest","isInterface":false,"isInternal":false}],"consumers":[{"name":"OrderPlacedHandler","identityType":"OrderPlacedHandler","identityTypeFullName":"MyApp.Handlers.OrderPlacedHandler"},{"name":"GetOrderStatusHandler","identityType":"GetOrderStatusHandler","identityTypeFullName":"MyApp.Handlers.GetOrderStatusHandler"}],"routes":{"inbound":[{"kind":"subscribe","messageTypeIdentity":"msg:OrderPlacedEvent","consumerName":"OrderPlacedHandler","endpoint":{"name":"my-service.order-placed","address":"rabbitmq://localhost/my-service.order-placed","transportName":"RabbitMQ"}},{"kind":"request","messageTypeIdentity":"msg:GetOrderStatusRequest","consumerName":"GetOrderStatusHandler","endpoint":{"name":"get-order-status","address":"rabbitmq://localhost/get-order-status","transportName":"RabbitMQ"}}],"outbound":[{"kind":"publish","messageTypeIdentity":"msg:OrderPlacedEvent","endpoint":{"name":"order-placed","address":"rabbitmq://localhost/order-placed","transportName":"RabbitMQ"}},{"kind":"send","messageTypeIdentity":"msg:GetOrderStatusRequest","endpoint":{"name":"get-order-status","address":"rabbitmq://localhost/get-order-status","transportName":"RabbitMQ"}}]},"sagas":[]}],"transports":[{"identifier":"rabbitmq://localhost:5672/","name":"RabbitMQ","schema":"rabbitmq","transportType":"RabbitMQMessagingTransport","receiveEndpoints":[{"name":"my-service.order-placed","kind":"default","address":"rabbitmq://localhost/my-service.order-placed","source":{"address":"rabbitmq://localhost:5672/q/my-service.order-placed"}},{"name":"get-order-status","kind":"default","address":"rabbitmq://localhost/get-order-status","source":{"address":"rabbitmq://localhost:5672/q/get-order-status"}}],"dispatchEndpoints":[{"name":"order-placed","kind":"default","address":"rabbitmq://localhost/order-placed","destination":{"address":"rabbitmq://localhost:5672/e/order-placed"}},{"name":"get-order-status","kind":"default","address":"rabbitmq://localhost/get-order-status","destination":{"address":"rabbitmq://localhost:5672/q/get-order-status"}}],"topology":{"address":"rabbitmq://localhost:5672/","entities":[{"kind":"exchange","name":"order-placed","address":"rabbitmq://localhost:5672/e/order-placed","flow":"inbound","properties":{"type":"fanout","durable":true,"autoDelete":false,"autoProvision":true}},{"kind":"exchange","name":"my-service.order-placed","address":"rabbitmq://localhost:5672/e/my-service.order-placed","flow":"inbound","properties":{"type":"fanout","durable":true,"autoDelete":false,"autoProvision":true}},{"kind":"queue","name":"my-service.order-placed","address":"rabbitmq://localhost:5672/q/my-service.order-placed","flow":"outbound","properties":{"durable":true,"exclusive":false,"autoDelete":false,"autoProvision":true}},{"kind":"queue","name":"get-order-status","address":"rabbitmq://localhost:5672/q/get-order-status","flow":"outbound","properties":{"durable":true,"exclusive":false,"autoDelete":false,"autoProvision":true}}],"links":[{"kind":"bind","address":"rabbitmq://localhost:5672/b/e/order-placed/e/my-service.order-placed","source":"rabbitmq://localhost:5672/e/order-placed","target":"rabbitmq://localhost:5672/e/my-service.order-placed","direction":"forward","properties":{"routingKey":null,"autoProvision":true}},{"kind":"bind","address":"rabbitmq://localhost:5672/b/e/my-service.order-placed/q/my-service.order-placed","source":"rabbitmq://localhost:5672/e/my-service.order-placed","target":"rabbitmq://localhost:5672/q/my-service.order-placed","direction":"forward","properties":{"routingKey":null,"autoProvision":true}}]}}]}' />

That registration produces:

- A **receive endpoint** named `my-service.order-placed` (subscribe route, bound to `OrderPlacedHandler`)
- A **receive endpoint** named `get-order-status` (request route, bound to `GetOrderStatusHandler`)
- A **dispatch endpoint** for publishing `OrderPlacedEvent`
- A **dispatch endpoint** for sending `GetOrderStatusRequest`
- A **reply receive endpoint** for inbound responses
- A **reply dispatch endpoint** for outbound responses
- **Error endpoints** (`_error` suffix) for each receive endpoint - destination of the `Fault` middleware when a handler throws
- **Skipped endpoints** (`_skipped` suffix) for each receive endpoint - destination of the `DeadLetter` middleware when no consumer matched the message

All derived from your handler types and message types through naming conventions.

```mermaid
flowchart
    subgraph Registration
        H1[OrderPlacedHandler]
        H2[GetOrderStatusHandler]
    end

    subgraph ReceiveEndpoints["Receive Endpoints"]
        RE1["my-service.order-placed"]
        RE2["get-order-status"]
        RE3["reply-..."]
    end

    subgraph DispatchEndpoints["Dispatch Endpoints"]
        DE1["order-placed (exchange)"]
        DE2["get-order-status (queue)"]
        DE3["replies (queue)"]
    end

    H1 --> RE1
    H2 --> RE2

    RE1 <-->|broker| DE1
    RE2 <-->|broker| DE2
    RE3 <-->|broker| DE3
```

This is the default behavior: you declare what you handle, and the framework derives the endpoints and routes from those declarations. You can configure everything manually when you need to override a convention.

# How routing works

Mocha maintains two kinds of routes that work together to move messages between services.

## Inbound routes

An inbound route connects a message type to a receive endpoint. When you register a handler with `.AddEventHandler<T>()` or `.AddRequestHandler<T>()`, Mocha creates an inbound route that tells the transport which messages to deliver to which consumer.

| Handler interface                           | Route kind | Endpoint type                                 |
| ------------------------------------------- | ---------- | --------------------------------------------- |
| `IEventHandler<T>`, `IBatchEventHandler<T>` | Subscribe  | Queue bound to an exchange or topic (fan-out) |
| `IEventRequestHandler<TRequest>`            | Send       | Dedicated queue (point-to-point)              |
| `IEventRequestHandler<TRequest, TResponse>` | Request    | Dedicated queue (point-to-point)              |

## Outbound routes

An outbound route connects a message type to a dispatch endpoint. When you call `bus.PublishAsync<T>()` or `bus.SendAsync()`, Mocha looks up the outbound route for the message type and dispatches through the corresponding endpoint.

**Routing priority:** Mocha resolves outbound routes in this order:

1. If an explicit route is registered with `AddMessage<T>()`, use it.
2. Otherwise, derive the endpoint name from naming conventions.

When a message goes somewhere unexpected, open the topology visualizer first - it shows you the complete routing picture at a glance. If you need to dig deeper, check for an explicit `AddMessage<T>()` registration, then check what the conventions produce.

## Startup-time topology

Mocha resolves all endpoints and builds broker topology when the bus starts, not when the first message is sent. If your exchange or queue configuration is invalid - a mis-spelled exchange name, an incompatible binding - you will know at startup. Topology errors surface immediately, not silently on the first `SendAsync` call.

This design is reflected in the [Message Endpoint](https://www.enterpriseintegrationpatterns.com/patterns/messaging/MessageEndpoint.html) pattern: an endpoint bridges your application code to the messaging infrastructure, and that bridge is established at initialization time.

# Naming conventions

Mocha derives endpoint names automatically from your handler and message types. PascalCase type names become kebab-case; common suffixes (`Handler`, `Consumer`, `Command`, `Event`, `Message`, `Query`, `Response`) are stripped.

## Set the service name

For subscribe (pub/sub) endpoints, the naming convention prefixes the endpoint name with the service name:

```csharp
builder.Services
    .AddMessageBus()
    .Host(h => h.ServiceName("order-service"))
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ();
```

The receive endpoint is named `order-service.order-placed`. Without the `.Host()` call, the service name defaults to the `SERVICE_NAME` or `OTEL_SERVICE_NAME` environment variable, or falls back to the entry assembly name.

> **Why the service prefix?**
>
> Events use fan-out delivery: a single published message is delivered to every subscribing service. For fan-out to work correctly with point-to-point queues, each subscribing service needs its own queue. Without a service-specific prefix, two services consuming the same event would share a single queue and compete for messages - each service would only process half the events.
>
> The service prefix is what makes each service's queue unique. See [Point-to-Point Channel](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PointToPointChannel.html) for the full explanation of why fan-out and point-to-point channels work this way.

## Receive endpoint naming

For **subscribe** routes (event handlers), the endpoint name combines the service name with the handler name:

| Handler type              | Service name | Endpoint name                |
| ------------------------- | ------------ | ---------------------------- |
| `OrderPlacedEventHandler` | `catalog`    | `catalog.order-placed-event` |
| `BillingHandler`          | `billing`    | `billing.billing`            |
| `OrderAuditConsumer`      | `audit`      | `audit.order-audit`          |

The `Handler` and `Consumer` suffixes are stripped. The service name prefix ensures each service gets its own queue for the same event type.

For **send** and **request** routes, the endpoint name comes from the message type directly, without a service prefix:

| Message type              | Endpoint name       |
| ------------------------- | ------------------- |
| `ReserveInventoryCommand` | `reserve-inventory` |
| `ProcessRefundCommand`    | `process-refund`    |
| `GetProductRequest`       | `get-product`       |

Send endpoints are shared across services: any service sending `ReserveInventoryCommand` dispatches to the same `reserve-inventory` queue. There is only one destination for a command - that's the point-to-point guarantee.

## Publish endpoint naming

For publish (fan-out) endpoints, the name includes the message namespace in kebab-case:

| Message type            | Namespace               | Endpoint name                             |
| ----------------------- | ----------------------- | ----------------------------------------- |
| `OrderPlacedEvent`      | `Demo.Contracts.Events` | `demo.contracts.events.order-placed`      |
| `PaymentCompletedEvent` | `Demo.Contracts.Events` | `demo.contracts.events.payment-completed` |

## Special endpoint names

| Purpose       | Name pattern         | Example                                     |
| ------------- | -------------------- | ------------------------------------------- |
| Error queue   | `{endpoint}_error`   | `catalog.order-placed-event_error`          |
| Skipped queue | `{endpoint}_skipped` | `catalog.order-placed-event_skipped`        |
| Reply queue   | `response-{guid:N}`  | `response-3f2504e04f8911d39a0c0305e82c3301` |

The two failure-side endpoints are populated by different middlewares:

- **Error queue (`_error`)** receives messages whose handler threw an exception. The `Fault` middleware (`ReceiveFaultMiddleware`) catches the exception, attaches `fault-*` headers (exception type, message, stack trace, timestamp), and forwards the original envelope to the configured `ErrorEndpoint`.
- **Skipped queue (`_skipped`)** receives messages that completed the pipeline without any consumer marking them as consumed. The `DeadLetter` middleware (`ReceiveDeadLetterMiddleware`) re-dispatches the original envelope to the configured `SkippedEndpoint`.

Reply queues are temporary, per-instance queues used for request/reply correlation.

# Customize outbound routes

To override where a message is sent or published, use `AddMessage<T>()` with a route configuration:

```csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedHandler>()
    .AddMessage<OrderPlacedEvent>(m =>
    {
        m.Publish(r => r.ToExchange("custom-orders-exchange"));
    })
    .AddRabbitMQ();
```

`OrderPlacedEvent` now publishes to `custom-orders-exchange` instead of the convention-derived name. This is an explicit route - it takes priority over naming conventions. The receive endpoint is unaffected; it still subscribes based on the handler's message type.

For send (point-to-point) routes:

```csharp
builder.Services
    .AddMessageBus()
    .AddMessage<ProcessPaymentCommand>(m =>
    {
        m.Send(r => r.ToQueue("payment-processing-queue"));
    })
    .AddRabbitMQ();
```

Use these extension methods to target specific destination types when configuring outbound routes:

| Method             | URI Scheme  | Example                                    |
| ------------------ | ----------- | ------------------------------------------ |
| `ToQueue(name)`    | `queue:`    | `#!csharp r.ToQueue("payment-queue")`      |
| `ToExchange(name)` | `exchange:` | `#!csharp r.ToExchange("events-exchange")` |
| `ToTopic(name)`    | `topic:`    | `#!csharp r.ToTopic("orders.placed")`      |

The URI schemes (`queue:`, `exchange:`, `topic:`) tell Mocha what kind of transport entity to target. `queue:` addresses a point-to-point queue directly. `exchange:` addresses a fan-out exchange (RabbitMQ) or equivalent. `topic:` addresses a topic-based routing entity. The transport interprets these schemes and maps them to its native concepts.

To bypass routing entirely and send to a specific address at call time, pass a `SendOptions`:

```csharp
await bus.SendAsync(new ReserveInventoryCommand
{
    OrderId = orderId,
    ProductId = productId,
    Quantity = 3
},
new SendOptions
{
    Endpoint = new Uri("queue:custom-inventory-queue")
},
cancellationToken);
```

The queue does not have to be declared by the host. On RabbitMQ, `queue:<name>` or the transport form `rabbitmq:q/<name>` reaches a queue the host never declared, for example a queue another service owns. Under explicit binding, the host records that queue in its topology with auto-provisioning disabled and never declares, binds, or consumes it. A message the broker cannot route, for example because the queue does not exist, is dropped.

# Customize queues and binding

Use `transport.Queue("name")` as the primary API when you need to customize receive topology. The queue builder starts with the queue as the unit of configuration. It can declare the queue, bind handlers or consumers, opt into convention bindings, and configure receive settings in one place.

Calling `Queue("name")` by itself creates an infrastructure queue. Adding `Handler<T>()`, `Consumer<T>()`, or `Receives<T>()` materializes a receive endpoint for that queue.

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

        transport.Queue("combined-orders")
            .BindImplicitly()
            .MaxConcurrency(5)
            .FaultEndpoint("order-errors")
            .SkippedEndpoint("order-skipped")
            .Handler<OrderPlacedHandler>()
            .Handler<PaymentReceivedHandler>();
    });
```

Both handlers now consume from the same `combined-orders` queue. Without explicit transport binding, they would each get their own convention-derived endpoint.

## Bind handlers or message types

Use `.Handler<T>()` or `.Consumer<T>()` when you know the concrete handler or consumer types that should run on the queue:

```csharp
transport.Queue("combined-orders")
    .BindImplicitly()
    .Handler<OrderPlacedHandler>()
    .Handler<PaymentReceivedHandler>();

transport.Queue("audit-orders")
    .BindImplicitly()
    .Consumer<OrderAuditConsumer>();
```

Use `.Receives<T>()` when the queue should receive a message type and Mocha should connect all registered handlers for that message type:

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

        transport.Queue("all-orders")
            .BindImplicitly()
            .Receives<OrderPlaced>();
    });
```

Both `OrderPlacedHandler` and `OrderAuditHandler` now receive from the `all-orders` queue. This is topology-first design: you declare what messages a queue handles, and Mocha wires up all registered handlers.

Use `.Handler<T>()` when you know which handler types to bind. Use `.Receives<T>()` when you care about the message type and want all handlers for that type automatically connected. The same queue can use both approaches:

```csharp
transport.Queue("orders")
    .BindImplicitly()
    .Receives<OrderPlaced>()
    .Handler<OrderAuditHandler>();
```

If a message type is declared with `.Receives<T>()` but no handler is registered, Mocha throws an exception at startup.

## Understand implicit and explicit binding

Binding has two scopes: transport and queue.

At the transport scope, `BindImplicitly()` is the default. The transport auto-discovers registered handlers, creates convention-named queues, and adds the convention-derived exchange, topic, or subscription bindings for the messages each handler consumes.

```csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedHandler>()      // -> my-service.order-placed
    .AddEventHandler<PaymentReceivedHandler>()  // -> my-service.payment-received
    .AddRabbitMQ(transport =>
    {
        transport.BindImplicitly(); // default
    });
```

`BindExplicitly()` at the transport scope turns off that auto-discovery. Use it when the queues you define with `Queue("name")` should be the complete receive topology for that transport.

At the queue scope, `BindImplicitly()` keeps convention-derived source bindings for the message types handled by that queue. This is the common combination for custom queue names:

```csharp
transport.BindExplicitly();

transport.Queue("order-processing")
    .BindImplicitly()
    .Receives<OrderPlaced>();
```

`BindExplicitly()` at the queue scope suppresses convention-derived source bindings for that queue. Use it when you bind the queue to a specific source yourself, for example with `BindFrom(...)` or a transport-specific topology declaration.

```csharp
transport.BindExplicitly();

transport.Queue("regional-orders")
    .BindExplicitly()
    .BindFrom(new Uri("exchange:region-events"), "eu.*")
    .Handler<OrderPlacedHandler>();
```

## Configure convention endpoints

Use `transport.Handler<T>()` or `transport.Consumer<T>()` at the end of the transport configuration when you want to keep the convention-derived queue name and only tune the endpoint for one handler or consumer.

This is useful for small endpoint changes, but `Queue("name")` is the preferred surface when the queue name, queue topology, or multiple handler bindings are part of the customization.

```csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ(rabbit =>
    {
        rabbit.Handler<OrderPlacedHandler>()
            .ConfigureEndpoint(ep => ep
                .MaxConcurrency(5)
                .FaultEndpoint("order-errors")
                .SkippedEndpoint("order-skipped"));
    });
```

The same pattern works for consumers:

```csharp
rabbit.Consumer<OrderAuditConsumer>()
    .ConfigureEndpoint(ep => ep.MaxConcurrency(3));
```

Inside `ConfigureEndpoint()`, you have access to transport-specific settings. To set prefetch on a RabbitMQ endpoint:

```csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedHandler>()
    .AddRabbitMQ(rabbit =>
    {
        rabbit.Handler<OrderPlacedHandler>()
            .ConfigureEndpoint(ep => ep.MaxPrefetch(50));
    });
```

For PostgreSQL, configure the batch size:

```csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<OrderPlacedHandler>()
    .AddPostgres(transport =>
    {
        transport.Handler<OrderPlacedHandler>()
            .ConfigureEndpoint(ep => ep.MaxBatchSize(100));
    });
```

See the [RabbitMQ](./transports/rabbitmq.md), [PostgreSQL](./transports/postgres.md), and [InMemory](./transports/in-memory.md) transport pages for the full set of transport-specific queue and endpoint settings.

## Multi-transport handler routing

In a multi-transport setup, `Handler<T>()` also determines which transport owns the handler. Mark one transport as the default with `.IsDefaultTransport()`, then claim specific handlers on other transports:

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

A claimed handler is bound to the claiming transport regardless of which transport is the default. Unclaimed handlers fall through to the default transport. This is the recommended pattern for multi-transport routing - it avoids `BindExplicitly()` and keeps the configuration minimal.

## Temporary endpoints and per-instance identity

Call `Temporary()` on a receive endpoint or queue descriptor to scope its underlying infrastructure to the lifetime of the consuming process instead of provisioning it durably:

```csharp
transport.Queue($"tenant-events-{instanceId}")
    .Temporary()
    .Receives<TenantEvent>();
```

`instanceId` here is a value your host or application supplies - a process GUID, a pod name, an assigned worker ID. Mocha does not generate or append an instance identity to endpoint or queue names on your behalf. `Queue(name)` always uses the complete string you pass as the endpoint name and as the broker entity name; `GetReceiveEndpointName` applies the same naming conventions described above regardless of `Temporary()`, and never appends an instance segment.

`Temporary()` is a lifecycle intent, not a message setting. It controls how long the endpoint's backing queue exists, not how long an individual message on that queue lives - do not confuse it with a message TTL.

A uniquely named temporary queue binds to its publish topic or exchange the same way any other subscribe endpoint does. If every running instance calls `#!csharp Queue($"tenant-events-{instanceId}")` with its own `instanceId`, each instance gets its own queue bound to the same source, so every live instance receives a copy of every published message. If two instances instead pass the same queue name, they share one queue and become competing consumers on it - each message goes to only one of them.

Each transport maps `Temporary()` to a different native mechanism:

| Transport         | Mapping                                                                                           |
| ----------------- | ------------------------------------------------------------------------------------------------- |
| Azure Service Bus | `AutoDeleteOnIdle` on the queue (24-hour default, `Temporary(TimeSpan)` for a custom idle window) |
| RabbitMQ          | A non-durable, auto-delete queue                                                                  |
| PostgreSQL        | `AutoDelete` on the queue row, cascaded from the owning consumer's heartbeat and expiry           |
| InMemory          | API parity only - a temporary queue's lifetime is the hosting process's own runtime disposal      |

See the transport pages under [Transports](./transports/index.md) for the full mapping, defaults, and conflict-detection behavior for each transport.

## Dispatch endpoints

For outbound endpoints, use `DispatchEndpoint("name")`:

```csharp
builder.Services
    .AddMessageBus()
    .AddRabbitMQ(transport =>
    {
        transport.DispatchEndpoint("custom-dispatch")
            .Publish<OrderPlacedEvent>()
            .Send<ProcessPaymentCommand>();
    });
```

# Scope precedence

Configuration in Mocha follows a three-level scope hierarchy: **bus > transport > endpoint**. The most specific scope wins.

```text
Bus (global defaults)
  -> Transport (transport-specific overrides)
    -> Endpoint (per-endpoint overrides)
```

This applies to middleware pipelines, circuit breakers, concurrency limiters, and any feature that can be configured at multiple levels:

```csharp
builder.Services
    .AddMessageBus()
    .AddConcurrencyLimiter(opts => opts.MaxConcurrency = 20) // bus-level default
    .AddRabbitMQ(transport =>
    {
        transport.AddConcurrencyLimiter(opts => opts.MaxConcurrency = 10); // transport override

        transport.Endpoint("high-throughput")
            .MaxConcurrency(50); // endpoint override
    });
```

The `high-throughput` endpoint processes 50 messages concurrently. All other RabbitMQ endpoints use 10. Endpoints on other transports use the bus default of 20.

The middleware pipeline is compiled per-endpoint from the same three layers: bus middleware runs first, then transport middleware, then endpoint middleware. This means a retry policy registered at the bus level applies everywhere, but you can add an extra circuit breaker only for a specific endpoint. Other pages in this documentation reference this scope hierarchy as the canonical model - it governs middleware, reliability features, and observability configuration uniformly.

# Next steps

Your routing and endpoint configuration is set. From here:

- [**Middleware and Pipelines**](./middleware-and-pipelines.md) - Write custom middleware, control pipeline ordering, and understand how the three pipeline stages interact. Want to customize the processing pipeline? That's the next page.
- [**Reliability**](./reliability.md) - Configure fault handling, circuit breakers, concurrency limits, the transactional outbox, and the idempotent inbox.
- [**Transports**](./transports/index.md) - Dive into transport-specific configuration for RabbitMQ and InMemory.

---

### Website Content Docs Mocha Sagas (`website/content/docs/mocha/sagas.md`)

---
title: "Sagas"
description: "Learn how to orchestrate long-running business processes with saga state machines in Mocha, including state management, persistence, and parallel coordination."
---

The saga pattern was introduced by Garcia-Molina & Salem in 1987 as a way to manage long-lived transactions without holding distributed locks. In a messaging system, sagas implement the [Process Manager pattern](https://www.enterpriseintegrationpatterns.com/patterns/messaging/ProcessManager.html) - they coordinate a sequence of messages, track state across them, and drive a business process to completion. Mocha sagas use orchestration-style coordination: one central state machine issues commands and waits for replies, rather than choreographing services through shared events. See [microservices.io](https://microservices.io/patterns/data/saga.html) and the [Microsoft Azure Architecture: Saga pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/saga) for broader context on the pattern.

A saga loads or creates state when a message arrives, applies the configured transition, dispatches any side-effects (publish or send), and persists the result. When the saga reaches a final state, its persisted state is deleted and an optional response is sent back to the originator.

# When to use sagas vs. handlers

| Scenario                                     | Use a handler | Use a saga |
| -------------------------------------------- | ------------- | ---------- |
| Single message in, single action out         | Yes           | No         |
| Process completes in one step                | Yes           | No         |
| Process spans multiple messages over time    | No            | Yes        |
| Need to coordinate parallel operations       | No            | Yes        |
| Need persistent state across failures        | No            | Yes        |
| Need to send a response after multiple steps | No            | Yes        |

Handlers are simpler and appropriate when a single message triggers a single action. Sagas add value when the workflow requires multiple steps, waits for replies, or must survive process restarts.

A common mistake is using a saga for work that fits in a handler. If your handler calls `SendAsync` and does not need to wait for a reply before responding, a plain handler is sufficient. Reach for a saga when the process must pause and resume based on future messages.

# State machine diagram

The refund saga built in the tutorial below has three states. Each arrow is labeled with the message that triggers the transition:

```mermaid
stateDiagram-v2
    [*] --> AwaitingRefund : RequestQuickRefundRequest\n(create state, send ProcessRefundCommand)
    AwaitingRefund --> Completed : ProcessRefundResponse\n(update state with result)
    Completed --> [*] : send QuickRefundResponse\n(delete persisted state)
```

This is the shape you will build in the tutorial. Keep this diagram in mind as you work through the steps - each code block maps to one of these transitions.

<MochaTopologyVisualization data='{"services":[{"host":{"serviceName":"BasicSaga","assemblyName":"BasicSaga","instanceId":"99abf31b-e5a5-4881-826c-6850c42a05d5"},"messageTypes":[{"identity":"urn:message:mocha.events:not-acknowledged-event","runtimeType":"NotAcknowledgedEvent","runtimeTypeFullName":"Mocha.Events.NotAcknowledgedEvent","isInterface":false,"isInternal":true,"enclosedMessageIdentities":["urn:message:mocha.events:not-acknowledged-event"]},{"identity":"urn:message:mocha.events:acknowledged-event","runtimeType":"AcknowledgedEvent","runtimeTypeFullName":"Mocha.Events.AcknowledgedEvent","isInterface":false,"isInternal":true,"enclosedMessageIdentities":["urn:message:mocha.events:acknowledged-event"]},{"identity":"urn:message:global:process-refund-command","runtimeType":"ProcessRefundCommand","runtimeTypeFullName":"ProcessRefundCommand","isInterface":false,"isInternal":false,"enclosedMessageIdentities":["urn:message:global:process-refund-command","urn:message:mocha:i-event-request[process-refund-response]","urn:message:mocha:i-event-request"]},{"identity":"urn:message:global:process-refund-response","runtimeType":"ProcessRefundResponse","runtimeTypeFullName":"ProcessRefundResponse","isInterface":false,"isInternal":false,"enclosedMessageIdentities":["urn:message:global:process-refund-response"]},{"identity":"urn:message:global:request-quick-refund-request","runtimeType":"RequestQuickRefundRequest","runtimeTypeFullName":"RequestQuickRefundRequest","isInterface":false,"isInternal":false,"enclosedMessageIdentities":["urn:message:global:request-quick-refund-request","urn:message:mocha:i-event-request[quick-refund-response]","urn:message:mocha:i-event-request"]},{"identity":"urn:message:global:quick-refund-response","runtimeType":"QuickRefundResponse","runtimeTypeFullName":"QuickRefundResponse","isInterface":false,"isInternal":false,"enclosedMessageIdentities":["urn:message:global:quick-refund-response"]}],"consumers":[{"name":"Reply","identityType":"ReplyConsumer","identityTypeFullName":"Mocha.ReplyConsumer","isBatch":false},{"name":"ProcessRefundCommandHandler","identityType":"ProcessRefundCommandHandler","identityTypeFullName":"ProcessRefundCommandHandler","isBatch":false},{"name":"quick-refund-saga","identityType":"QuickRefundSaga","identityTypeFullName":"QuickRefundSaga","sagaName":"quick-refund-saga","isBatch":false}],"routes":{"inbound":[{"kind":"request","messageTypeIdentity":"urn:message:global:process-refund-command","consumerName":"ProcessRefundCommandHandler","endpoint":{"name":"process-refund","address":"memory://localhost/process-refund","transportName":"memory"}},{"kind":"request","messageTypeIdentity":"urn:message:global:request-quick-refund-request","consumerName":"quick-refund-saga","endpoint":{"name":"request-quick-refund-request","address":"memory://localhost/request-quick-refund-request","transportName":"memory"}},{"kind":"reply","messageTypeIdentity":"urn:message:global:process-refund-response","consumerName":"quick-refund-saga","endpoint":{"name":"Replies","address":"memory://localhost/Replies","transportName":"memory"}},{"kind":"reply","consumerName":"Reply","endpoint":{"name":"Replies","address":"memory://localhost/Replies","transportName":"memory"}}],"outbound":[{"kind":"send","messageTypeIdentity":"urn:message:global:process-refund-command","endpoint":{"name":"q/process-refund","address":"memory://localhost/q/process-refund","transportName":"memory"}},{"kind":"send","messageTypeIdentity":"urn:message:global:request-quick-refund-request","endpoint":{"name":"q/request-quick-refund-request","address":"memory://localhost/q/request-quick-refund-request","transportName":"memory"}}]},"sagas":[{"name":"quick-refund-saga","stateType":"RefundSagaState","stateTypeFullName":"RefundSagaState","consumerName":"quick-refund-saga","states":[{"name":"__Initial","isInitial":true,"isFinal":false,"onEntry":{},"transitions":[{"eventType":"RequestQuickRefundRequest","eventTypeFullName":"RequestQuickRefundRequest","transitionTo":"AwaitingRefund","transitionKind":"request","autoProvision":true,"send":[{"messageType":"ProcessRefundCommand","messageTypeFullName":"ProcessRefundCommand"}]}]},{"name":"AwaitingRefund","isInitial":false,"isFinal":false,"onEntry":{},"transitions":[{"eventType":"ProcessRefundResponse","eventTypeFullName":"ProcessRefundResponse","transitionTo":"Completed","transitionKind":"reply","autoProvision":true}]},{"name":"Completed","isInitial":false,"isFinal":true,"onEntry":{},"response":{"eventType":"QuickRefundResponse","eventTypeFullName":"QuickRefundResponse"},"transitions":[]}]}]}],"transports":[{"identifier":"memory://basicsaga/","name":"memory","schema":"memory","transportType":"InMemoryMessagingTransport","receiveEndpoints":[{"name":"Replies","kind":"reply","address":"memory://localhost/Replies","source":{"address":"memory://basicsaga/q/response-99abf31be5a54881826c6850c42a05d5"}},{"name":"process-refund","kind":"default","address":"memory://localhost/process-refund","source":{"address":"memory://basicsaga/q/process-refund"}},{"name":"request-quick-refund-request","kind":"default","address":"memory://localhost/request-quick-refund-request","source":{"address":"memory://basicsaga/q/request-quick-refund-request"}}],"dispatchEndpoints":[{"name":"Replies","kind":"reply","address":"memory://localhost/Replies","destination":{"address":"memory://basicsaga/q/response-99abf31be5a54881826c6850c42a05d5"}},{"name":"q/process-refund","kind":"default","address":"memory://localhost/q/process-refund","destination":{"address":"memory://basicsaga/q/process-refund"}},{"name":"q/request-quick-refund-request","kind":"default","address":"memory://localhost/q/request-quick-refund-request","destination":{"address":"memory://basicsaga/q/request-quick-refund-request"}}],"topology":{"address":"memory://basicsaga/","entities":[{"kind":"topic","name":".process-refund","address":"memory://basicsaga/e/.process-refund","flow":"inbound"},{"kind":"topic","name":"process-refund","address":"memory://basicsaga/e/process-refund","flow":"inbound"},{"kind":"topic","name":".request-quick-refund-request","address":"memory://basicsaga/e/.request-quick-refund-request","flow":"inbound"},{"kind":"topic","name":"request-quick-refund-request","address":"memory://basicsaga/e/request-quick-refund-request","flow":"inbound"},{"kind":"queue","name":"response-99abf31be5a54881826c6850c42a05d5","address":"memory://basicsaga/q/response-99abf31be5a54881826c6850c42a05d5","flow":"outbound"},{"kind":"queue","name":"process-refund","address":"memory://basicsaga/q/process-refund","flow":"outbound"},{"kind":"queue","name":"request-quick-refund-request","address":"memory://basicsaga/q/request-quick-refund-request","flow":"outbound"}],"links":[{"kind":"bind","address":"memory://basicsaga/b/t/.process-refund/t/process-refund","source":"memory://basicsaga/e/.process-refund","target":"memory://basicsaga/e/process-refund","direction":"forward"},{"kind":"bind","address":"memory://basicsaga/b/t/process-refund/q/process-refund","source":"memory://basicsaga/e/process-refund","target":"memory://basicsaga/q/process-refund","direction":"forward"},{"kind":"bind","address":"memory://basicsaga/b/t/.request-quick-refund-request/t/request-quick-refund-request","source":"memory://basicsaga/e/.request-quick-refund-request","target":"memory://basicsaga/e/request-quick-refund-request","direction":"forward"},{"kind":"bind","address":"memory://basicsaga/b/t/request-quick-refund-request/q/request-quick-refund-request","source":"memory://basicsaga/e/request-quick-refund-request","target":"memory://basicsaga/q/request-quick-refund-request","direction":"forward"}]}}]}' trace='{"traceId":"saga-orchestration-trace-001","activities":[{"id":"so-1","parentId":null,"startTime":"2024-06-15T10:30:00.000Z","durationMs":120,"status":"ok","operation":"send","messageType":"RequestQuickRefundRequest","messageTypeIdentity":"urn:message:global:request-quick-refund-request","transport":"memory"},{"id":"so-2","parentId":"so-1","startTime":"2024-06-15T10:30:00.001Z","durationMs":2,"status":"ok","operation":"dispatch","messageType":"RequestQuickRefundRequest","messageTypeIdentity":"urn:message:global:request-quick-refund-request","endpointName":"q/request-quick-refund-request","endpointAddress":"memory://localhost/q/request-quick-refund-request","transport":"memory"},{"id":"so-3","parentId":"so-2","startTime":"2024-06-15T10:30:00.008Z","durationMs":3,"status":"ok","operation":"receive","messageType":"RequestQuickRefundRequest","messageTypeIdentity":"urn:message:global:request-quick-refund-request","endpointName":"request-quick-refund-request","endpointAddress":"memory://localhost/request-quick-refund-request","transport":"memory"},{"id":"so-4","parentId":"so-3","startTime":"2024-06-15T10:30:00.011Z","durationMs":25,"status":"ok","operation":"consume","messageType":"RequestQuickRefundRequest","messageTypeIdentity":"urn:message:global:request-quick-refund-request","consumerName":"quick-refund-saga"},{"id":"so-5","parentId":"so-4","startTime":"2024-06-15T10:30:00.012Z","durationMs":22,"status":"ok","operation":"saga-transition","sagaName":"quick-refund-saga","sagaInstanceId":"3f2504e0-4f89-11d3-9a0c-0305e82c3301","fromState":"__Initial","toState":"AwaitingRefund","eventType":"RequestQuickRefundRequest"},{"id":"so-6","parentId":"so-5","startTime":"2024-06-15T10:30:00.013Z","durationMs":2,"status":"ok","operation":"dispatch","messageType":"ProcessRefundCommand","messageTypeIdentity":"urn:message:global:process-refund-command","endpointName":"q/process-refund","endpointAddress":"memory://localhost/q/process-refund","transport":"memory"},{"id":"so-7","parentId":"so-6","startTime":"2024-06-15T10:30:00.020Z","durationMs":3,"status":"ok","operation":"receive","messageType":"ProcessRefundCommand","messageTypeIdentity":"urn:message:global:process-refund-command","endpointName":"process-refund","endpointAddress":"memory://localhost/process-refund","transport":"memory"},{"id":"so-8","parentId":"so-7","startTime":"2024-06-15T10:30:00.023Z","durationMs":20,"status":"ok","operation":"consume","messageType":"ProcessRefundCommand","messageTypeIdentity":"urn:message:global:process-refund-command","consumerName":"ProcessRefundCommandHandler"},{"id":"so-9","parentId":"so-8","startTime":"2024-06-15T10:30:00.043Z","durationMs":2,"status":"ok","operation":"dispatch","messageType":"ProcessRefundResponse","messageTypeIdentity":"urn:message:global:process-refund-response","endpointName":"Replies","endpointAddress":"memory://localhost/Replies","transport":"memory"},{"id":"so-10","parentId":"so-9","startTime":"2024-06-15T10:30:00.050Z","durationMs":3,"status":"ok","operation":"receive","messageType":"ProcessRefundResponse","messageTypeIdentity":"urn:message:global:process-refund-response","endpointName":"Replies","endpointAddress":"memory://localhost/Replies","transport":"memory"},{"id":"so-11","parentId":"so-10","startTime":"2024-06-15T10:30:00.053Z","durationMs":15,"status":"ok","operation":"consume","messageType":"ProcessRefundResponse","messageTypeIdentity":"urn:message:global:process-refund-response","consumerName":"quick-refund-saga"},{"id":"so-12","parentId":"so-11","startTime":"2024-06-15T10:30:00.054Z","durationMs":12,"status":"ok","operation":"saga-transition","sagaName":"quick-refund-saga","sagaInstanceId":"3f2504e0-4f89-11d3-9a0c-0305e82c3301","fromState":"AwaitingRefund","toState":"Completed","eventType":"ProcessRefundResponse"}]}' />

> [!WARNING]
> **messages can arrive out of order.**
>
> In a distributed system, message delivery order is not guaranteed. If your saga handles two message types that could both initiate the process, configure both as initial transitions. Do not assume the first message you define in code is the first message the saga will receive in production.

# Tutorial: Build a refund saga

By the end of this section, you will have a working saga that receives a refund request, sends a command to the billing service, and returns a response when the refund completes.

## Define the saga state

Saga state must extend `SagaStateBase`. This base class provides `Id` (unique saga instance identifier), `State` (current state name), `Errors` (error history), and `Metadata` (custom key-value data).

```csharp
using Mocha.Sagas;

namespace MyApp.Sagas;

public class RefundSagaState : SagaStateBase
{
    // Order information captured when the saga starts
    public required Guid OrderId { get; init; }
    public required decimal Amount { get; init; }
    public required string CustomerId { get; init; }
    public required string Reason { get; init; }

    // Results populated during processing
    public Guid? RefundId { get; set; }
    public decimal? RefundedAmount { get; set; }
    public string? FailureReason { get; set; }

    // Factory method to create state from the initiating request
    public static RefundSagaState FromQuickRefund(RequestQuickRefundRequest request)
        => new()
        {
            OrderId = request.OrderId,
            Amount = request.Amount,
            CustomerId = request.CustomerId,
            Reason = request.Reason
        };

    // Helper to create the command sent to billing
    public ProcessRefundCommand ToProcessRefund()
        => new()
        {
            OrderId = OrderId,
            Amount = Amount,
            Reason = Reason,
            CustomerId = CustomerId
        };
}
```

Properties marked `required` are set when the saga starts. Mutable properties (`set`) are updated as transitions execute. The factory method converts the initiating message into the state object. Helper methods create the commands the saga dispatches to other services.

## Define the message contracts

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

## Define the saga

Subclass `Saga<TState>` and override `Configure` to define the state machine.

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

The state machine has three states:

1. **Initial** - receives `RequestQuickRefundRequest`, creates `RefundSagaState`, sends `ProcessRefundCommand` to billing, transitions to `AwaitingRefund`.
2. **AwaitingRefund** - receives `ProcessRefundResponse` from billing, updates state with the result, transitions to `Completed`.
3. **Completed** (final) - builds `QuickRefundResponse` from state and sends it back to the original caller. The persisted saga state is then deleted.

## Register the saga

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddMessageBus()
    .AddSaga<QuickRefundSaga>()
    .AddRabbitMQ();

var app = builder.Build();
app.Run();
```

`.AddSaga<T>()` registers the saga with the bus. The saga's consumer is created automatically and listens for the message types defined in the state machine transitions.

> [!TIP]
> Sagas are discovered automatically by the source generator. If you use `Add{ModuleName}()` from [Handler Registration](./handler-registration.md), you do not need to register sagas manually - the source generator calls `AddSaga<T>()` for every concrete `Saga<TState>` subclass it finds.

## Trigger the saga

From the sender side, use `RequestAsync` to start the saga and await the final response:

```csharp
var response = await bus.RequestAsync(
    new RequestQuickRefundRequest
    {
        OrderId = orderId,
        Amount = 49.99m,
        CustomerId = "customer-42",
        Reason = "Defective product"
    },
    cancellationToken);

Console.WriteLine($"Refund {response.RefundId}: success={response.Success}");
```

Expected output:

```text
info: Mocha.Sagas.Saga[0]
      Created saga state QuickRefundSaga 3f2504e0-4f89-11d3-9a0c-0305e82c3301
info: Mocha.Sagas.Saga[0]
      Entering state QuickRefundSaga Initial
info: Mocha.Sagas.Saga[0]
      Sending event QuickRefundSaga ProcessRefundCommand
info: Mocha.Sagas.Saga[0]
      Transitioning state QuickRefundSaga AwaitingRefund by event ProcessRefundResponse
info: Mocha.Sagas.Saga[0]
      Entering state QuickRefundSaga Completed
info: Mocha.Sagas.Saga[0]
      Replying to saga QuickRefundSaga ... QuickRefundResponse
info: Mocha.Sagas.Saga[0]
      Saga completed QuickRefundSaga 3f2504e0-4f89-11d3-9a0c-0305e82c3301
Refund d4c3b2a1-...: success=True
```

The saga receives the request, sends a command to billing, waits for the reply, builds a response, and completes. The caller's `RequestAsync` resolves with the typed `QuickRefundResponse`.

# How-to guides

## Coordinate parallel operations

When a saga needs to wait for multiple replies before proceeding, model each combination as a separate state. The `ReturnProcessingSaga` demonstrates this pattern - after inspection, it sends both a refund command and a restock command in parallel, then handles whichever reply arrives first.

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

The key insight: `AwaitingBothReplies` has two transitions, one for each reply type. Whichever arrives first moves the saga to a "one done, waiting for the other" state. The second reply completes the saga. This avoids race conditions without explicit locking.

## Start a saga from an event

Not all sagas begin with a request/reply. Use `.OnEvent<T>()` instead of `.OnRequest<T>()` when the saga is initiated by a published event.

```csharp
descriptor
    .Initially()
    .OnEvent<ReturnPackageReceivedEvent>()
    .StateFactory(RefundSagaState.FromReturnPackageReceived)
    .Send((_, state) => state.ToInspectReturn())
    .TransitionTo(AwaitingInspection);
```

When using `.OnEvent<T>()`, the saga does not capture a reply address. No response is sent when the saga completes. Use `.OnRequest<T>()` when the caller expects a response via `RequestAsync`.

## Publish events from transitions

To publish an event as a side-effect of a transition, use `.Publish()` on the transition descriptor:

```csharp
descriptor
    .During(AwaitingRefund)
    .OnReply<ProcessRefundResponse>()
    .Then((state, response) =>
    {
        state.RefundId = response.RefundId;
        state.RefundedAmount = response.Amount;
    })
    .Publish((_, state) => new RefundCompletedEvent
    {
        OrderId = state.OrderId,
        RefundId = state.RefundId!.Value,
        Amount = state.RefundedAmount!.Value
    })
    .TransitionTo(Completed);
```

`.Publish()` dispatches the event after the transition action runs. The saga automatically adds the `saga-id` header to published messages so downstream consumers can correlate them back to the saga instance.

## Publish or send on state entry

To dispatch messages when entering a state (not tied to a specific transition), use `.OnEntry()` on the state descriptor:

```csharp
descriptor
    .During(AwaitingInspection)
    .OnEntry()
    .Publish(state => new InspectionStartedEvent
    {
        OrderId = state.OrderId,
        ReturnId = state.ReturnId!.Value
    });
```

On-entry actions run every time the saga enters that state, regardless of which transition caused it.

## Handle faults and compensation

### Compensating transactions

When a service-spanning process fails partway through, you cannot roll back completed steps the way a database transaction would. Instead, you issue compensating transactions - commands that logically undo the work already done. For example, if a refund succeeds but restocking fails, you may need to reverse the refund.

This is a fundamental property of distributed systems: without distributed ACID isolation, partial failure is always possible. The [Microsoft Azure Architecture documentation](https://learn.microsoft.com/en-us/azure/architecture/patterns/saga) classifies transactions in a saga as compensable (can be undone), pivot (the point of no return), or retryable (will eventually succeed). Design your `OnFault()` handlers with this taxonomy in mind.

### Implement fault handling

When a command sent by a saga fails, the receiving service sends a `NotAcknowledgedEvent` back instead of the expected reply. Use `OnFault()` to define a transition that handles this case and runs compensation logic.

```csharp
descriptor
    .During(AwaitingRefund)
    .OnFault()
    .Then((state, fault) =>
    {
        state.FailureReason = fault.ErrorMessage;
        state.FailureStage = "Refund";
    })
    .Send((_, state) => new ReverseChargeCommand
    {
        OrderId = state.OrderId,
        Amount = state.Amount
    })
    .TransitionTo("Compensating");
```

`OnFault()` is an extension method on `ISagaStateDescriptor<TState>` that registers a transition for `NotAcknowledgedEvent`. The fault record carries `ErrorCode`, `ErrorMessage`, `CorrelationId`, and `MessageId` so your compensation logic can identify what failed.

For sagas that send multiple commands in parallel, add `OnFault()` transitions in each waiting state. This ensures you can compensate regardless of which step failed.

## Handle timeouts

Long-running sagas may need to protect against the case where an expected reply never arrives. Use `OnTimeout()` to define what happens when the saga has been waiting too long.

### Schedule a timeout when entering a waiting state

Register the timeout as an on-entry action on the state that needs a deadline:

```csharp
descriptor
    .During(AwaitingRefund)
    .OnEntry()
    .ScheduleTimeout(TimeSpan.FromMinutes(5));  // fire SagaTimedOutEvent after 5 minutes

descriptor
    .During(AwaitingRefund)
    .OnTimeout()
    .Then((state, _) =>
    {
        state.FailureReason = "Refund timed out after waiting 5 minutes";
    })
    .Send((_, state) => new ReverseChargeCommand
    {
        OrderId = state.OrderId,
        Amount = state.Amount
    })
    .TransitionTo("TimedOut");

descriptor.Finally("TimedOut")
    .Respond(state => new QuickRefundResponse
    {
        OrderId = state.OrderId,
        Success = false,
        FailureReason = state.FailureReason,
        CompletedAt = DateTimeOffset.UtcNow
    });
```

When the saga transitions out of `AwaitingRefund` via a normal reply, the scheduled timeout is cancelled automatically. If the timeout fires first, `OnTimeout()` runs the configured transition - in this case, dispatching compensation and moving to `TimedOut`.

### What to do when a timeout fires

A fired timeout means the downstream service did not reply within the expected window. Common responses:

- Transition to a compensation state and issue undo commands for completed steps.
- Transition to a final state and send a failure response back to the original caller.
- Transition to a retry state and re-send the original command (only if idempotent).

Do not leave the saga in the same waiting state after a timeout. The saga must advance so it does not wait forever.

## Configure saga persistence with Postgres

By default, saga state is stored in memory and lost on restart. For production, use a persistent store. Mocha provides a Postgres-backed saga store via Entity Framework Core.

**1. Add the EF Core entity to your `DbContext`.**

```csharp
using Microsoft.EntityFrameworkCore;
using Mocha.Sagas.EfCore;

public class CatalogDbContext : DbContext
{
    public DbSet<SagaState> SagaStates => Set<SagaState>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Configure the saga state table
        modelBuilder.AddPostgresSagas();
    }
}
```

**2. Register the Postgres saga store with the bus.**

```csharp
builder.Services
    .AddMessageBus()
    .AddSaga<QuickRefundSaga>()
    .AddSaga<ReturnProcessingSaga>()
    .AddEntityFramework<CatalogDbContext>(p =>
    {
        p.AddPostgresSagas();
    })
    .AddRabbitMQ();
```

**3. Create an EF Core migration.**

```shell
dotnet ef migrations add Sagas
dotnet ef database update
```

## Create a saga with the fluent API

For simple sagas that do not need a dedicated class, use `Saga.Create<TState>()`:

```csharp
var saga = Saga.Create<RefundSagaState>(descriptor =>
{
    descriptor
        .Initially()
        .OnRequest<RequestQuickRefundRequest>()
        .StateFactory(RefundSagaState.FromQuickRefund)
        .Send((_, state) => state.ToProcessRefund())
        .TransitionTo("AwaitingRefund");

    descriptor
        .During("AwaitingRefund")
        .OnReply<ProcessRefundResponse>()
        .Then((state, response) =>
        {
            state.RefundId = response.RefundId;
            state.RefundedAmount = response.Amount;
        })
        .TransitionTo("Completed");

    descriptor
        .Finally("Completed")
        .Respond(state => new QuickRefundResponse
        {
            OrderId = state.OrderId,
            Success = state.RefundId.HasValue,
            RefundId = state.RefundId,
            RefundedAmount = state.RefundedAmount,
            FailureReason = state.FailureReason,
            CompletedAt = DateTimeOffset.UtcNow
        });
});
```

This produces the same state machine as the class-based approach. The fluent API is useful for tests and prototyping.

# Concurrency

If two messages for the same saga instance arrive simultaneously - for example, two parallel replies landing within milliseconds of each other - one succeeds and the other retries. The Postgres saga store uses optimistic concurrency with a version column. The second writer detects the version mismatch, reloads the latest state, and retries the transition.

This retry is automatic and transparent. You do not need to handle it in your saga code. However, if a saga endpoint processes a very high volume of concurrent messages for the same instance, retry contention can add latency. In that case, consider hosting high-concurrency sagas on dedicated endpoints with constrained parallelism.

# How saga correlation works

When a saga sends a command, Mocha attaches a `saga-id` header to the outgoing message. When the reply arrives, the saga runtime reads this header to find the existing saga instance and load its persisted state.

For event-initiated sagas, correlation uses the `ICorrelatable` interface:

```csharp
using Mocha.Sagas;

public sealed record SagaTimedOutEvent(Guid SagaId) : ICorrelatable
{
    public Guid? CorrelationId => SagaId;
}
```

Messages that implement `ICorrelatable` are matched to saga instances by their `CorrelationId`. This is how sagas handle events that are not direct replies to commands the saga sent.

The correlation lookup order is:

1. Check if the message implements `ICorrelatable` and has a non-null `CorrelationId`.
2. Check the message headers for a `saga-id` header.
3. If neither is found, treat the message as an initiating event and create a new saga instance.

# Timeouts

A saga that waits for a message that never arrives will stay in its current state forever. Timeouts ensure every saga eventually completes - either through normal processing or by timing out.

Mocha provides a saga-level `Timeout()` API that sets a single deadline for the entire saga instance. The timeout is scheduled when the saga is created and automatically cancelled when the saga reaches any final state.

> **Prerequisites:** `Timeout()` schedules a `SagaTimedOutEvent` through the same scheduled message store used by `ScheduleSendAsync`: the PostgreSQL transport's own store, the InMemory transport's own store (registered automatically by `AddInMemory()`, non-durable), or the `UsePostgresScheduling()` fallback for RabbitMQ. If no store is available when the saga is created, the timeout is silently skipped: the saga proceeds without an automatic timeout and only receives a `SagaTimedOutEvent` if one is sent manually. See [Scheduling: Set up store-based scheduling](./scheduling.md#set-up-store-based-scheduling-for-rabbitmq) for setup instructions.

## Configure a saga-level timeout

Call `Timeout()` on the saga descriptor to set a deadline that applies to the entire saga instance:

```csharp
protected override void Configure(ISagaDescriptor<OrderState> descriptor)
{
    // 30-minute timeout - saga will time out if it doesn't reach a final state
    descriptor.Timeout(TimeSpan.FromMinutes(30))
        .Respond(state => new OrderTimedOutResponse(state.Id));

    descriptor.Initially()
        .OnEvent<OrderPlacedEvent>()
            .StateFactory(e => new OrderState { OrderId = e.OrderId })
            .TransitionTo("AwaitingPayment");

    // Handle timeout in any non-final state
    descriptor.DuringAny()
        .OnTimeout()
            .TransitionTo(StateNames.TimedOut);

    descriptor.During("AwaitingPayment")
        .OnEvent<PaymentReceivedEvent>()
            .TransitionTo("Completed");

    descriptor.Finally("Completed");
}
```

`Timeout(TimeSpan)` does three things:

1. Creates a "Timed Out" final state (named `StateNames.TimedOut`).
2. Returns an `ISagaFinalStateDescriptor<TState>` so you can chain `.Respond()` to send a response when the saga times out.
3. Tells the saga framework to schedule a timeout event when a new saga instance is created.

The timeout clock starts when the saga is created - that is, when the first event arrives and a new instance is provisioned. If the saga reaches any final state before the deadline, the pending timeout is automatically cancelled. If the timeout fires, a `SagaTimedOutEvent` is delivered to the saga instance.

Use `OnTimeout()` on a state descriptor to define what happens when the timeout arrives. Handle it the same way you handle any other event - run `.Then()` actions, dispatch commands, or transition to a different state.

## Key behaviors

| Behavior            | Detail                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Scope               | Per-saga instance, not per-state. The deadline covers the entire saga lifetime.                                                                                                                                                                                                                                                                                             |
| Duration            | Fixed at configuration time via `TimeSpan`.                                                                                                                                                                                                                                                                                                                                 |
| Auto-cancellation   | When the saga reaches any final state (normal completion, error final state, etc.), the pending timeout is cancelled.                                                                                                                                                                                                                                                       |
| Late delivery       | If the timeout fires after the saga was already deleted, the event is silently dropped.                                                                                                                                                                                                                                                                                     |
| Missing handler     | If no `OnTimeout()` handler is configured for the current state, the saga throws an execution error. See [troubleshooting](#timeout-troubleshooting) below.                                                                                                                                                                                                                 |
| Recommended pattern | `DuringAny().OnTimeout()` handles the timeout regardless of which state the saga is in.                                                                                                                                                                                                                                                                                     |
| Response on timeout | Chain `.Respond()` on the timed-out final state to send a response back to the original requester.                                                                                                                                                                                                                                                                          |
| Scheduling store    | Requires a scheduled message store to schedule the timeout: the PostgreSQL transport's own store, the InMemory transport's own store (non-durable), or the `UsePostgresScheduling()` fallback for RabbitMQ. Without a store, the timeout is silently skipped rather than throwing - see [Scheduling](./scheduling.md#set-up-store-based-scheduling-for-rabbitmq) for setup. |

## Timeout troubleshooting

**Timeout never fires.**
Verify that a scheduled message store is configured for the saga's transport: the PostgreSQL transport's own store (registered automatically by `AddPostgres()`), the InMemory transport's own store (registered automatically by `AddInMemory()`), or the `UsePostgresScheduling()` fallback for RabbitMQ. Without one, the saga catches the resulting `NotSupportedException` and silently proceeds without scheduling a timeout - no error is raised, so a saga you expect to time out will simply never receive a `SagaTimedOutEvent`.

**"SagaExecutionException: No transition defined for SagaTimedOutEvent."**
You configured `Timeout()` but did not add an `OnTimeout()` handler. Add a catch-all handler:

```csharp
descriptor.DuringAny()
    .OnTimeout()
    .TransitionTo(StateNames.TimedOut);
```

# API reference

## Saga descriptor

| Method      | Available on              | Parameters         | Description                                                                                                                                              |
| ----------- | ------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Initially` | `ISagaDescriptor<TState>` | -                  | Returns the initial state descriptor for defining transitions that create new saga instances.                                                            |
| `During`    | `ISagaDescriptor<TState>` | `string stateName` | Returns a state descriptor for defining transitions in the named state.                                                                                  |
| `DuringAny` | `ISagaDescriptor<TState>` | -                  | Returns a catch-all state descriptor whose transitions apply to all non-initial, non-final states.                                                       |
| `Finally`   | `ISagaDescriptor<TState>` | `string stateName` | Declares a final state. When the saga enters this state, persisted state is deleted and an optional response is sent.                                    |
| `Timeout`   | `ISagaDescriptor<TState>` | `TimeSpan timeout` | Creates a timed-out final state and schedules automatic timeout on saga creation. Returns `ISagaFinalStateDescriptor<TState>` for chaining `.Respond()`. |

## State transitions

| Method         | Available on                   | Parameters | Description                                                                                 |
| -------------- | ------------------------------ | ---------- | ------------------------------------------------------------------------------------------- |
| `OnEvent<T>`   | `ISagaStateDescriptor<TState>` | -          | Registers a transition triggered by a published event.                                      |
| `OnRequest<T>` | `ISagaStateDescriptor<TState>` | -          | Registers a transition triggered by a request message (captures reply address).             |
| `OnReply<T>`   | `ISagaStateDescriptor<TState>` | -          | Registers a transition triggered by a reply to a previously sent command.                   |
| `OnFault`      | `ISagaStateDescriptor<TState>` | -          | Registers a transition triggered by a `NotAcknowledgedEvent`.                               |
| `OnTimeout`    | `ISagaStateDescriptor<TState>` | -          | Registers a transition for `SagaTimedOutEvent`. Sugar for `OnRequest<SagaTimedOutEvent>()`. |

## Transition actions

| Method         | Available on                                | Parameters                     | Description                                                                        |
| -------------- | ------------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------- |
| `StateFactory` | `ISagaTransitionDescriptor<TState, TEvent>` | `Func<TEvent, TState>`         | Creates new saga state from the initiating event. Required on initial transitions. |
| `Then`         | `ISagaTransitionDescriptor<TState, TEvent>` | `Action<TState, TEvent>`       | Runs a synchronous action to update saga state.                                    |
| `Send`         | `ISagaTransitionDescriptor<TState, TEvent>` | `Func<TEvent, TState, object>` | Sends a command as a side-effect of the transition.                                |
| `Publish`      | `ISagaTransitionDescriptor<TState, TEvent>` | `Func<TEvent, TState, object>` | Publishes an event as a side-effect of the transition.                             |
| `TransitionTo` | `ISagaTransitionDescriptor<TState, TEvent>` | `string stateName`             | Moves the saga to the named state.                                                 |

## Final state

| Method    | Available on                        | Parameters             | Description                                                                       |
| --------- | ----------------------------------- | ---------------------- | --------------------------------------------------------------------------------- |
| `Respond` | `ISagaFinalStateDescriptor<TState>` | `Func<TState, object>` | Sends a response to the original requester when the saga enters this final state. |

## Lifecycle

| Method          | Available on                   | Parameters | Description                                                                                |
| --------------- | ------------------------------ | ---------- | ------------------------------------------------------------------------------------------ |
| `OnEntry`       | `ISagaStateDescriptor<TState>` | -          | Returns a lifecycle descriptor for actions that run every time the saga enters the state.  |
| `WhenCompleted` | `ISagaDescriptor<TState>`      | -          | Returns a lifecycle descriptor for actions that run when the saga reaches any final state. |

# Troubleshooting

**Saga state is lost on restart.**
Saga state is stored in memory by default. For production, configure a persistent store. See [Configure saga persistence with Postgres](#configure-saga-persistence-with-postgres).

**"No transition defined" exception.**
The saga received a message in a state that has no matching transition. Verify that every state has transitions for all expected message types. Use `DuringAny()` for catch-all transitions that apply across all non-final states.

**Saga never completes.**
Check that the downstream service is running and sending replies. If the saga is waiting for a reply that never arrives, consider adding a [timeout](#timeouts) to ensure the saga eventually reaches a final state.

**Two messages arrive for the same saga instance simultaneously.**
The Postgres saga store uses optimistic concurrency. The second writer detects the version mismatch, reloads state, and retries automatically. See [Concurrency](#concurrency).

# Next steps

Understand how transports work in [Transports](./transports/index.md).

> **Runnable examples:** [BasicSaga](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Sagas/BasicSaga), [ParallelSaga](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Examples/Sagas/ParallelSaga)
>
> **Full demo:** [Demo.Catalog](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/examples/Demo/Demo.Catalog) implements two production-style sagas: `QuickRefundSaga` (a simple two-state refund flow) and `ReturnProcessingSaga` (a complex multi-state saga with parallel inspection, restocking, and refund steps).

---

### Website Content Docs Mocha Scheduling (`website/content/docs/mocha/scheduling.md`)

---
title: "Scheduling"
description: "Schedule messages for future delivery in Mocha using absolute times or relative delays, with durable Postgres persistence or non-durable in-memory delivery."
---

Sometimes a message should not be delivered right now. A welcome email goes out 30 minutes after signup. A payment retry fires 24 hours after the first failure. A saga timeout triggers if no response arrives within 5 minutes. Scheduling lets you hand a message to the bus with a future delivery time, and the infrastructure takes care of the rest. If plans change, you can cancel a scheduled message before it is dispatched.

```csharp
var result = await bus.SchedulePublishAsync(
    new SendWelcomeEmail { UserId = userId },
    DateTimeOffset.UtcNow.AddMinutes(30),
    cancellationToken);

// result.Token can be used to cancel the message later
```

The call returns immediately with a `SchedulingResult`. The message is persisted and delivered when the scheduled time arrives. If you need to revoke the message before delivery, pass the token to `CancelScheduledMessageAsync`.

# Schedule a message

Mocha provides scheduling methods on `IMessageBus` for scheduling with an absolute `DateTimeOffset`.

## Schedule with an absolute time

Use a `DateTimeOffset` when you know the exact delivery time:

```csharp
var scheduledTime = DateTimeOffset.UtcNow.AddHours(24);

// Schedule a publish (fan-out to all subscribers)
var publishResult = await bus.SchedulePublishAsync(
    new PaymentRetryEvent { OrderId = orderId },
    scheduledTime,
    cancellationToken);

// Schedule a send (directed to a single handler)
var sendResult = await bus.ScheduleSendAsync(
    new CleanupExpiredSessionsCommand { CutoffTime = cutoff },
    scheduledTime,
    cancellationToken);
```

Both methods return a `SchedulingResult` with a `Token` you can use for cancellation and an `IsCancellable` flag that tells you whether cancellation is supported by the current scheduling infrastructure.

## Schedule with options

The scheduling methods also accept an options overload. If you need to combine scheduling with other options like expiration or custom headers, pass a `PublishOptions` or `SendOptions` struct:

```csharp
var result = await bus.SchedulePublishAsync(
    new PaymentRetryEvent { OrderId = orderId },
    DateTimeOffset.UtcNow.AddHours(24),
    new PublishOptions
    {
        ExpirationTime = DateTimeOffset.UtcNow.AddHours(48),
        Headers = new Dictionary<string, object?> { ["priority"] = "high" }
    },
    cancellationToken);
```

```csharp
var result = await bus.ScheduleSendAsync(
    new RetryPaymentCommand { PaymentId = paymentId },
    DateTimeOffset.UtcNow.AddMinutes(30),
    new SendOptions
    {
        ExpirationTime = DateTimeOffset.UtcNow.AddHours(1)
    },
    cancellationToken);
```

You can also set `ScheduledTime` directly on options when calling `PublishAsync` or `SendAsync`. This approach does not return a `SchedulingResult`, so you cannot cancel the message later:

```csharp
await bus.PublishAsync(
    new PaymentRetryEvent { OrderId = orderId },
    new PublishOptions
    {
        ScheduledTime = DateTimeOffset.UtcNow.AddHours(24),
        ExpirationTime = DateTimeOffset.UtcNow.AddHours(48),
    },
    cancellationToken);
```

# Cancel a scheduled message

When a scheduled message is no longer needed, cancel it before the scheduled time arrives. The `SchedulingResult` returned by `SchedulePublishAsync` and `ScheduleSendAsync` contains the token you need.

```csharp
// Schedule a payment reminder
var result = await bus.SchedulePublishAsync(
    new PaymentReminderEvent { OrderId = orderId },
    DateTimeOffset.UtcNow.AddHours(24),
    cancellationToken);

// Customer pays before the reminder fires - cancel it
var cancelled = await bus.CancelScheduledMessageAsync(
    result.Token!,
    cancellationToken);
```

`CancelScheduledMessageAsync` returns `true` if the message was successfully cancelled and `false` otherwise.

## When cancellation returns false

A `false` return does not necessarily mean something went wrong. It means the message is no longer in the store:

- **Already dispatched.** The scheduled time passed and the message was delivered. The cancellation window has closed.
- **Already cancelled.** A previous call already removed the message. Cancelling twice is safe - the second call returns `false`.
- **Token not found.** The token does not match any message in the store.

## SchedulingResult

Every call to `SchedulePublishAsync` or `ScheduleSendAsync` returns a `SchedulingResult`:

```csharp
var result = await bus.SchedulePublishAsync(message, scheduledTime, cancellationToken);

if (result.IsCancellable)
{
    // Store the token so you can cancel later
    await SaveTokenAsync(result.Token!);
}
```

| Property        | Type             | Description                                                                               |
| --------------- | ---------------- | ----------------------------------------------------------------------------------------- |
| `Token`         | `string?`        | An opaque token for cancelling this message, or `null` if cancellation is not supported.  |
| `ScheduledTime` | `DateTimeOffset` | The time at which the message is scheduled for delivery.                                  |
| `IsCancellable` | `bool`           | `true` when the scheduling infrastructure supports cancellation and a token was assigned. |

The scheduling middleware is always part of the dispatch pipeline. When you set a `ScheduledTime`, it resolves a scheduled message store for the current transport, the transport's own store first (for example the PostgreSQL, InMemory, or Azure Service Bus native store), then the `UsePostgresScheduling()` fallback store if one is registered. `IsCancellable` is `true` whenever a store was found and persisted the message. If no store is available for the transport, scheduling does not silently fall back to native delivery, it throws `NotSupportedException`.

## Real-world example: cancellable reminder

A common pattern is scheduling a reminder that should be revoked when the user completes the expected action.

```csharp
public class OrderService(IMessageBus bus, IOrderRepository orders)
{
    public async Task PlaceOrderAsync(Order order, CancellationToken ct)
    {
        await orders.SaveAsync(order, ct);

        // Remind the customer to pay in 24 hours
        var result = await bus.SchedulePublishAsync(
            new PaymentReminderEvent { OrderId = order.Id },
            DateTimeOffset.UtcNow.AddHours(24),
            ct);

        // Persist the token so we can cancel later
        if (result.IsCancellable)
        {
            order.ReminderToken = result.Token;
            await orders.SaveAsync(order, ct);
        }
    }

    public async Task ConfirmPaymentAsync(Guid orderId, CancellationToken ct)
    {
        var order = await orders.GetAsync(orderId, ct);

        // Payment received - cancel the reminder
        if (order.ReminderToken is not null)
        {
            await bus.CancelScheduledMessageAsync(order.ReminderToken, ct);
            order.ReminderToken = null;
            await orders.SaveAsync(order, ct);
        }
    }
}
```

# Set up store-based scheduling for RabbitMQ

The PostgreSQL, InMemory, and Azure Service Bus transports handle scheduling natively with no extra setup. RabbitMQ has no scheduling store of its own, so a scheduled dispatch on it throws `NotSupportedException` unless you configure a fallback: a Postgres-backed message store that persists scheduled messages and dispatches them through a background worker.

**1. Add the NuGet packages.**

```shell
dotnet add package Mocha.EntityFrameworkCore
dotnet add package Mocha.EntityFrameworkCore.Postgres
```

**2. Add the `ScheduledMessage` entity to your DbContext model.**

```csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.AddPostgresScheduledMessages();
}
```

This maps the `ScheduledMessage` entity to a `scheduled_messages` table with columns for the envelope, scheduled time, retry count, and error tracking.

**3. Register the scheduling services.**

```csharp
builder.Services
    .AddMessageBus()
    .AddEventHandler<PaymentRetryHandler>()
    .AddEntityFramework<AppDbContext>(p =>
    {
        p.UsePostgresScheduling();
    })
    .AddPostgres(connectionString);
```

| Call                             | Purpose                                                                                                                     |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `UsePostgresScheduling()`        | Registers everything needed for durable scheduling with Postgres, including the background worker and EF Core interceptors. |
| `AddPostgresScheduledMessages()` | Adds the `ScheduledMessage` entity configuration to the EF Core model.                                                      |

`UsePostgresScheduling()` sets up everything needed to persist scheduled messages in Postgres and dispatch them at the right time. Outgoing messages with a `ScheduledTime` are intercepted and written to the `scheduled_messages` table instead of being sent to the transport. A background worker continuously polls for due messages and dispatches them through the bus. EF Core interceptors signal the worker when `SaveChanges` or a transaction commit occurs, enabling low-latency wake-up.

When `UsePostgresScheduling()` is configured, `SchedulePublishAsync` and `ScheduleSendAsync` return cancellable results with tokens you can pass to `CancelScheduledMessageAsync`.

**4. Create the database migration.**

After adding the model configuration, generate and apply an EF Core migration:

```shell
dotnet ef migrations add AddScheduledMessages
dotnet ef database update
```

# Transport scheduling behavior

The PostgreSQL, InMemory, and Azure Service Bus transports each have a scheduling store of their own. RabbitMQ needs the `UsePostgresScheduling()` fallback; without it, a scheduled dispatch on RabbitMQ throws `NotSupportedException`.

| Transport         | Scheduling type                          | Durability                      | Cancellation support                        | Setup required                            |
| ----------------- | ---------------------------------------- | ------------------------------- | ------------------------------------------- | ----------------------------------------- |
| InMemory          | Native (transport-owned store)           | Non-durable, lost on restart    | Yes (token prefixed `in-memory-transport:`) | None                                      |
| PostgreSQL        | Native (transport-owned store)           | Durable, survives restarts      | Yes (token prefixed `postgres-transport:`)  | None                                      |
| RabbitMQ          | None, requires `UsePostgresScheduling()` | Durable, via the fallback store | Yes, via the fallback store                 | `UsePostgresScheduling()` + EF Core model |
| Azure Service Bus | Native (`ScheduleMessageAsync`)          | Durable, broker-managed         | Yes (native)                                | None                                      |

**InMemory:** The transport handles scheduling natively through its own scheduled message store, registered automatically when you call `AddInMemory()`. When you set `ScheduledTime`, a background worker holds the message in process memory and dispatches it once the scheduled time arrives. No additional setup is required. Cancellation is supported - the returned token is prefixed `in-memory-transport:` and can be passed to `CancelScheduledMessageAsync`. The store is not durable: it lives only in process memory, so any message still pending when the process stops is lost.

**PostgreSQL:** The transport handles scheduling natively through its own scheduled message store, registered automatically when you call `AddPostgres()`. When you set `ScheduledTime`, the transport writes a `scheduled_time` column alongside the message instead of sending it immediately, and only delivers it to consumers after the scheduled time has passed. No additional setup is required beyond the standard [PostgreSQL transport configuration](./transports/postgres.md). Cancellation is supported - the returned token is prefixed `postgres-transport:` and can be passed to `CancelScheduledMessageAsync`.

**RabbitMQ:** RabbitMQ does not support native message scheduling. Scheduling a message throws `NotSupportedException` unless you register `UsePostgresScheduling()` with an EF Core DbContext. Once configured, scheduled messages are persisted to a Postgres `scheduled_messages` table instead of reaching the RabbitMQ transport, and a background worker dispatches them at the scheduled time, routing through the RabbitMQ transport. Cancellation is fully supported - the returned token is prefixed `postgres-scheduler:` and the `SchedulingResult` contains it for use with `CancelScheduledMessageAsync`.

**Azure Service Bus:** Azure Service Bus supports both **native scheduling and native cancellation** with no additional infrastructure. When you call `SchedulePublishAsync` or `ScheduleSendAsync`, the dispatch endpoint uses `ServiceBusSender.ScheduleMessageAsync` and the broker holds the message until the scheduled time. The returned `SchedulingResult.Token` encodes the entity path and the broker-assigned sequence number, so `CancelScheduledMessageAsync` can revoke the message via `ServiceBusSender.CancelScheduledMessageAsync` without a Postgres store, EF Core model, or background worker. `IsCancellable` is `true` for every scheduled ASB message. See the [Azure Service Bus transport](./transports/azure-service-bus.md) page for the full setup.

## Retry behavior

This section applies to the Postgres-backed scheduled message store, used natively by the PostgreSQL transport or through the `UsePostgresScheduling()` fallback for RabbitMQ. If a scheduled message fails to dispatch, the scheduler retries with exponential backoff. Each failed attempt increases the wait time before the next retry. After 10 attempts (the default `max_attempts`), the message is no longer eligible for dispatch. You can inspect failed messages by querying the `scheduled_messages` table and checking the `last_error` column.

The InMemory transport's store does not retry failed dispatches. If a scheduled message fails to dispatch, it is dropped and the failure is logged.

## Multiple service instances

When multiple instances of your service are running, each scheduled message is processed by exactly one instance. There is no risk of duplicate delivery from the scheduler.

## Outbox integration

When both the transactional outbox and scheduling are configured, scheduled messages participate in the transaction correctly. Messages with a `ScheduledTime` are intercepted by the scheduler and never reach the outbox. Messages dispatched by the background worker skip both the scheduler and the outbox, going directly to the transport. See [Reliability](./reliability.md) for outbox configuration.

# Schedule messages in sagas

Saga transitions and lifecycle actions support scheduled message dispatch through dedicated extension methods. This is useful for saga timeouts, reminder patterns, and delayed side effects.

## Schedule in a transition

```csharp
public class OrderSagaConfiguration : SagaConfiguration<OrderState>
{
    public override void Configure()
    {
        x.Initially()
            .OnEvent<OrderPlacedEvent>()
            .StateFactory(_ => new OrderState())
            .ScheduledPublish(
                TimeSpan.FromMinutes(30),
                state => new OrderReminderEvent { OrderId = state.OrderId })
            .TransitionTo("AwaitingPayment");

        x.During("AwaitingPayment")
            .OnEvent<PaymentReceivedEvent>()
            .ScheduledSend(
                TimeSpan.FromHours(1),
                state => new GenerateInvoiceCommand { OrderId = state.OrderId })
            .TransitionTo("Completed");
    }
}
```

## Schedule in a lifecycle action

Lifecycle descriptors (actions that run on saga creation, completion, or finalization) also support scheduling:

```csharp
x.WhenCompleted()
    .ScheduledPublish(
        TimeSpan.FromDays(7),
        state => new OrderFeedbackRequestEvent { OrderId = state.OrderId });
```

Both `ScheduledPublish` and `ScheduledSend` are available on `ISagaTransitionDescriptor` and `ISagaLifeCycleDescriptor`. The factory receives the current saga state and returns the message to schedule.

For automatic saga timeouts that cancel themselves on completion, see [Timeouts](./sagas.md#timeouts) in the Sagas guide.

See [Sagas](./sagas.md) for the full saga configuration guide.

# Troubleshooting

**Scheduled messages are not being delivered.**
Check that the background worker is running. Look for `Scheduler sleeping until ...` log entries at `Information` level (emitted by the Postgres-backed worker; the in-memory worker does not log these). If there are no log entries for RabbitMQ, verify that `UsePostgresScheduling()` is registered in your service configuration - RabbitMQ has no scheduling store of its own, so it needs the fallback to deliver scheduled messages at all. `AddInMemory()` and `AddPostgres()` register their own stores and background workers automatically, so this step does not apply to those transports.

**Messages are delivered immediately instead of at the scheduled time.**
Messages scheduled for a time in the past are dispatched immediately. Verify that your `ScheduledTime` is in the future.

**"The message will be retried" warnings in logs.**
The Postgres-backed dispatcher logs at `Warning` level when it cannot read or route a scheduled message, and leaves the row in place for a later attempt by this or another instance. The log message names the cause: `Could not deserialize message body` means the stored envelope could not be parsed, `Could not resolve the message type` means the message type is not registered in the dispatching instance (for example after it was renamed or removed, or before a rolling deployment has reached every instance), and `Could not resolve a dispatch endpoint for the destination` means no transport in the dispatching instance can deliver to the stored destination address. Each attempt counts toward `max_attempts`. After the last attempt, the dispatcher logs `exhausted all ... retry attempts` at `Critical` level and the message is no longer eligible for dispatch.

**Scheduled messages fail repeatedly (Postgres-backed scheduling).**
The Postgres-backed dispatcher records each failure in the `last_error` column and retries with exponential backoff. After 10 attempts, the message is no longer eligible for dispatch. Query the `scheduled_messages` table and inspect the `last_error` column for diagnostics:

```sql
SELECT id, scheduled_time, times_sent, last_error
FROM scheduled_messages
WHERE times_sent >= max_attempts;
```

The in-memory transport does not retry or record errors: a scheduled message whose dispatch fails is dropped, and the failure is logged.

**Multiple service instances dispatch the same message.**
This does not happen with Postgres-backed scheduling: the dispatcher uses row-level locking to ensure each message is processed by exactly one instance. The in-memory transport is single-process, so this does not apply.

**Cancellation returns false even though I have a valid token.**
The message was already dispatched before the cancellation request reached the store. Once the background worker picks up a message and delivers it, the row is deleted and cancellation is no longer possible. With the Azure Service Bus transport, the broker returns `MessageNotFound` once the scheduled message has been enqueued for delivery, which surfaces here as `false`. If you need a wider cancellation window, schedule messages further in the future or check `SchedulingResult.IsCancellable` to confirm the infrastructure supports cancellation.

**`SchedulePublishAsync`/`ScheduleSendAsync` throws `NotSupportedException` instead of returning a result.**
No scheduled message store is available for the current transport. `SchedulingResult.IsCancellable` is `true` whenever a call succeeds, there is no store that persists a message without returning a cancellable token. Configure `UsePostgresScheduling()` with an EF Core DbContext as a fallback for RabbitMQ, or use the PostgreSQL, InMemory, or [Azure Service Bus](./transports/azure-service-bus.md) transport, each of which registers its own store automatically.

# Next steps

- [**Reliability**](./reliability.md) - Configure the transactional outbox and inbox for guaranteed delivery alongside scheduling.
- [**Sagas**](./sagas.md) - Build multi-step workflows with state machines, timeouts, and scheduled side effects.
- [**PostgreSQL Transport**](./transports/postgres.md) - Set up the Postgres transport that powers durable scheduling.
- [**Messaging Patterns**](./messaging-patterns.md) - Understand the difference between publish (fan-out) and send (point-to-point) when choosing which scheduling method to use.

---

### Website Content Docs Nitro Analyzers (`website/content/docs/nitro/analyzers.md`)

---
title: "Analyzers"
description: "The `ChilliCream.Nitro` Roslyn source generator emits an `AddDefaults()` method that wires up Nitro integration and warns when required packages are missing."
---

The `ChilliCream.Nitro` meta-package ships with a Roslyn source generator that simplifies Nitro integration setup. It provides compile-time warnings when required packages are missing and generates an `AddDefaults()` extension method that wires everything up in a single call.

# Compile-time warnings

If your project references HotChocolate or Fusion but is missing the corresponding Nitro integration package, you get a compile-time warning:

| Warning    | Trigger                                                           |
| ---------- | ----------------------------------------------------------------- |
| **NS0001** | HotChocolate referenced without `ChilliCream.Nitro.HotChocolate`  |
| **NS0002** | HotChocolate.Fusion referenced without `ChilliCream.Nitro.Fusion` |

# Generated `AddDefaults()` extension method

When the correct integration package is referenced, the generator emits an `AddDefaults()` extension method on `INitroBuilder`. This method wires up the default Nitro integration with the GraphQL pipeline in a single call.

```csharp
builder.Services
    .AddNitro(options =>
    {
        options.ApiId = "my-api";
        options.ApiKey = "my-key";
        options.Stage = "production";
    })
    .AddDefaults();
```

You can still customize per-schema options by calling `ModifyNitroOptions()` on the builder afterwards:

```csharp
builder.Services
    .AddNitro(options =>
    {
        options.ApiId = "my-api";
        options.ApiKey = "my-key";
    })
    .AddDefaults();

builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .ModifyNitroOptions(options =>
    {
        options.PersistedOperations.Enabled = true;
        options.Metrics.Enabled = true;
    });
```

# Troubleshooting

If `AddDefaults()` does not appear in IntelliSense:

1. Verify that `ChilliCream.Nitro` is referenced (this is the meta-package containing the source generator).
2. Verify that the matching integration package is referenced (`ChilliCream.Nitro.HotChocolate` for HotChocolate projects, `ChilliCream.Nitro.Fusion` for Fusion projects).
3. Rebuild the project so the source generator can run.

If you cannot use the source generator, call the explicit method instead: `.AddHotChocolate()` or `.AddFusion()` on the `INitroBuilder`.

---

### Website Content Docs Nitro Environments (`website/content/docs/nitro/environments.md`)

---
title: Environments
description: "Define environment variables in Nitro to reuse URLs, tokens, and credentials across documents and switch between development, testing, and production values."
---

Environment variable provide a mechanism for reusing specific values across multiple documents. These variables are defined as key-value pairs.

Frequently used data, like URLs, tokens, and credentials, are commonly centrally managed as environment variable.
By doing so, these values can be referenced in your documents and changed in a single location for all you documents. You can also quickly switch between different values for different environments.

For instance, you might store your API key as an environment variable, allowing it to be used universally in all your documents.
If the API key changes, you can just update the environment variable without having to manually alter each document.

You can define multiple environments for a single workspace corresponding to various contexts or project stages (e.g., development, testing, production). The environments can be switched in the status bar.

When you have different project stages such as such as development (`https://serivce1.dev.company.com`), testing (`https://serivce1.test.company.com`), and production (`https://serivce1.prod.company.com`), you can for example set up the dynamic part of the url as a variable URL and incorporate it in all your urls (`https://serivce1.{{subdomain}}.company.com`).
By switching the active environment in Nitro, the base URL will update automatically, streamlining your workflow.
Similarly, identifiers such as client ID and client secret can be defined as environment variable, which make it convenient to work with.

# Creating Environments


To create a new environment, follow these steps:

1. Click on the environment icon on the left side of your screen.
2. Click the Plus icon to create a new environment.
3. Enter a name for the environment.

# Rename Environments


You can rename by right-clicking on the environment and selecting the 'Rename' option.

# Specifying Variables



To open the environment variable editor, click on the environment you would like to edit.

1. **Enabled**: If an environment variable is disabled, it will not be used in requests. This state is not synchronized with the server and is only local to the client. This way you can temporarily disable an environment variable without other users being affected.
2. **Name**: The name of the environment variable. This name will be used to reference the variable in your documents. e.g. `{{ClientId}}`, `{{ClientSecret}}` `{{Subdomain}}`
3. **Value**: The value of the environment variable. This value will be substituted in your documents when the environment variable is referenced.
4. **Secret**: If an environment variable is marked as secret, it will be shown as a password field within the Nitro interface. This feature is designed to help prevent accidental exposure of the value. Please note that marking a value as secret does not mean it is encrypted; it is simply visually obscured in the user interface.
5. **Save Tab**: Clicking the 'Save Tab' button will save the environment variables.

# Switching Environments


To switch environments, click on the environment name in the status bar. A dropdown menu will appear, listing all available environments.
After selecting an environment, the environment variables of the selected environment will be active and applied to all documents.

# Using Variables


Once you have defined an environment variable, you can reference it in your documents by using the following syntax: `{{variable_name}}`.
Environment variable can be employed in a variety of places within Nitro, such as URLs, GraphQL Variables, and Connection Settings. When a request is executed, any referenced environment variable will be substituted with their respective values.

---

### Website Content Docs Nitro Nitro Services (`website/content/docs/nitro/nitro-services.md`)

---
title: Connect your API
description: "Connect your Hot Chocolate server to Nitro to fetch persisted operations from the client registry and report telemetry, using the ChilliCream.Nitro packages."
---

Nitro can be smoothly integrated into your HotChocolate server, enabling utilization of the Persisted Operation Storage found within the client registry, to report operations and collect open telemetry. Your server will establish a connection with Nitro, retrieving persisted operations based on their unique hashes. Additional information on the client registry can be found [here](./apis/client-registry.md).

# Getting Started

To get started, follow these steps:

1. Set up a client registry as instructed [here](./apis/client-registry.md).

2. Install the Nitro package from NuGet using the following command:

```shell
dotnet add package ChilliCream.Nitro
dotnet add package ChilliCream.Nitro.HotChocolate
```

3. Configure your services as shown in the following code snippet:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddNitro(x =>
    {
        x.ApiId = "VGhpcyBpcyBub3QgYSByZWFsIGFwaSBpZA==";
        x.ApiKey = "Tm9wZSwgdGhpcyBpcyBhbHNvIG5vIHJlYWwga2V5IDspIA==";
        x.Stage = "dev";
    });

builder
    .AddGraphQL()
    .AddQueryType<Query>()
    .AddInstrumentation() // if you want to use telemetry
    .UseOnlyPersistedOperationAllowed() // optional
    .UsePersistedOperationPipeline(); // if you want to use persisted operations

var app = builder.Build();

app.MapGraphQL();

app.Run();
```

4. Retrieve the API id and API key from Nitro using the `nitro api list` and `nitro api-key create` commands respectively. Instructions for these commands can be found [here](./cli/installation.md).

Congratulations! You have successfully integrated Nitro into your HotChocolate server. You can now publish new versions of your clients and your server will automatically retrieve the latest persisted operations.

---

### Website Content Docs Nitro Subscriptions (`website/content/docs/nitro/subscriptions.md`)

---
title: Subscription
description: "Manage your Nitro subscription in the management portal: create a Pro plan, change or cancel plans, and handle monthly or annual billing for your organization."
---

You can manage your subscriptions directly from the web portal, accessible from [here](https://identity.chillicream.com/Organization), or through Manage Organizations on the client.

# Creating a Subscription

When you do not have a subscription yet, you can create one in the 'Subscription' section of the management portal. Open the organization in the context of an owner or admin to see this section.


To create a Pro subscription, click the 'Select' button. You will be redirected to the billing portal to complete the payment process. Enter your payment details and click on the 'Subscribe' button to complete the payment process.

You have the option to pay monthly or annually. Annual payments are discounted.

You can cancel your subscription at any time. The subscription will remain active until the end of the current billing period. You can also change your subscription plan at any time. Check out the [Changing a Subscription](#changing-a-subscription) section for more information.

# Changing a Subscription

To change your subscription plan, follow these steps:

1. **Access the Management Portal:** Open the management portal by clicking ['Manage Organizations' -> 'Manage'](./organizations/index.md#managing-organizations) option in the context menu of Nitro or by navigating to [here](https://identity.chillicream.com/Organizations).

2. **Navigate to Subscription:** Within the management portal, click on 'Manage' in the subscription section.
   

3. **Update plan:** Here, you can update your subscription plan. Changing this value will affect the number of users who can join your organization.
   

# Expiry and Seats

Nitro's subscription model allows a certain number of users (seats) to join an organization based on your chosen subscription plan.

If your subscription expires, all users except the organization owner will be deactivated a few days after the expiration. The deactivated users will regain access once the subscription is renewed.

If your organization reaches its maximum seat limit, no new users will be able to join the organization. You can manage this by either upgrading your subscription to allow for more seats or by removing existing users from the organization to free up seats.

# Billing

In the client app you can directly access the billing portal by clicking 'Billing' and managing your running subscriptions:


The billing of an organization is currently on a per-user basis. All billing-related aspects, including subscription management and payment processing, are handled securely through Stripe, which you can directly manage from the management portal.

> Please note that the pricing model and subscription terms are subject to change, and users are advised to stay updated by regularly checking our website or contacting our customer support team.

---

### Website Content Docs Nitro Workspaces (`website/content/docs/nitro/workspaces.md`)

---
title: Workspaces
description: "Workspaces group APIs, documents, and environments within a Nitro organization; switch them in the status bar or create one with nitro workspace create."
---

Workspaces are logical groups within an organization. A workspace could represent a team, a department, or a group of APIs that are related to each other.

Each workspace has its own APIs, documents, API documents, environments, and more. You cannot share resources between workspaces.

All members of an organization have default access to all workspaces and can edit the documents within them. In the future we will add more granular permissions to workspaces.

In Nitro, you can switch between workspaces using the status bar.



At the moment, workspaces can only be created with the Nitro CLI. To create a new workspace, use the `nitro workspace create` command.
You can find out more about Nitro CLI [here](./cli/installation.md).

---

### Website Content Docs Skills Authoring Skills (`website/content/docs/skills/authoring-skills.md`)

---
title: "Authoring Skills"
description: "Author Agent Skills with the `skills` CLI: scaffold a folder with `dnx skills init`, write the SKILL.md frontmatter and instructions, and publish through git."
---

Package your team's conventions once, and any agent can pick them up. A skill is a folder with a `SKILL.md` file that tells an agent how to do something the way your team does it: your release checklist, your code review rules, your API client patterns. You write it once, push it to a git repo, and your teammates install it with one command. From then on, every agent they run loads that knowledge on demand, no copy-pasting prompts into each session.

This page teaches you to scaffold a skill, write a correct `SKILL.md`, add supporting files, and publish it so others can install it with [`dnx skills add`](./installing-skills.md). The skill format is the open [Agent Skills](https://agentskills.io/home) standard, so what you write here works across [55+ agents](./index.md), not only the one you author it in.

# Scaffold a skill

> Prerequisite: the .NET 10 SDK. See [Getting Started](./getting-started.md) before running the commands below.

To create a new skill in its own folder, run `dnx skills init` with a name.

```shell
dnx skills init my-skill
```

```text
Initialized skill: my-skill

Created:
  my-skill/SKILL.md

Next steps:
  1. Edit my-skill/SKILL.md to define your skill instructions
  2. Update the name and description in the frontmatter

Publishing:
  GitHub: Push to a repo, then skills add <owner>/<repo>
  URL:    Host the file, then skills add https://example.com/my-skill/SKILL.md
```

This creates `my-skill/SKILL.md`. If everything worked, you have a folder named `my-skill` with one file inside it.

To scaffold a skill in the current directory instead, run `dnx skills init` with no name. The CLI derives the skill name from the folder you are in and writes `SKILL.md` next to your other files.

```shell
dnx skills init
```

```text
Initialized skill: my-skill

Created:
  SKILL.md

Next steps:
  1. Edit SKILL.md to define your skill instructions
  2. Update the name and description in the frontmatter

Publishing:
  GitHub: Push to a repo, then skills add <owner>/<repo>
  URL:    Host the file, then skills add https://example.com/my-skill/SKILL.md
```

`dnx skills init` never overwrites an existing skill. If `SKILL.md` is already present, it leaves your file untouched and tells you so.

```text
Skill already exists at my-skill/SKILL.md
```

## The generated template

`dnx skills init` writes a spec-compliant starting point: the two required frontmatter fields, then a body with a heading and the sections most skills need.

```markdown
---
name: my-skill
description: A brief description of what this skill does
---

# my-skill

Instructions for the agent to follow when this skill is activated.

## When to use

Describe when this skill should be used.

## Instructions

1. First step
2. Second step
3. Additional steps as needed
```

Replace the placeholder `description`, then fill in the body. The next sections explain each part.

# Structure a SKILL.md

A `SKILL.md` has two parts: YAML frontmatter between `---` fences, and a Markdown body below it. The frontmatter is metadata the agent reads to decide whether your skill is relevant. The body is the instructions the agent follows once it decides to use the skill.

The "Required frontmatter" and "Optional frontmatter" subsections below are reference material: field-by-field constraint tables you can scan when you fill in the frontmatter. For the complete, authoritative field list see the [Reference](./reference.md).

## Required frontmatter

Every skill needs `name` and `description`. The CLI skips any skill that is missing either field, so getting these right is the difference between a skill that installs and one that silently disappears.

| Field         | Constraints                                                                                                                                                                                                                                               |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`        | 1 to 64 characters. Lowercase letters, digits, and hyphens only. No leading or trailing hyphen. No consecutive hyphens. Should match the skill's folder name. Must not contain XML tags, and must not contain the reserved words `anthropic` or `claude`. |
| `description` | 1 to 1024 characters. Must say BOTH what the skill does AND when to use it. Include specific trigger keywords that match the tasks where the skill applies. Must not contain XML tags.                                                                    |

The `description` is the single most important field you write. At startup the agent loads only your `name` and `description` (not the body), then matches the user's request against that description to decide whether to read the rest. A vague description means the agent never triggers your skill. Front-load what the skill does, then state the conditions that should activate it, and name the tools, file types, or commands involved.

```markdown
---
name: release-checklist
description: Runs our release checklist before publishing a NuGet package. Use when cutting a release, bumping a version, or preparing a package for nuget.org.
---
```

These constraints are the union of two authorities: the open spec at [agentskills.io/specification](https://agentskills.io/specification) (which adds the "match the folder name" and "no consecutive hyphens" rules) and the [Anthropic platform rules](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) (which add the reserved-word and XML-tag prohibitions). Honoring both keeps your skill portable across every agent.

## Optional frontmatter

Add these fields only when you need them. The open spec at [agentskills.io/specification](https://agentskills.io/specification) is the authoritative field list; the most useful fields are below.

| Field           | Purpose                                                                                                                                                                     |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `license`       | A license name or a reference to a bundled license file. Keep it short.                                                                                                     |
| `compatibility` | Up to 500 characters describing environment needs (intended product, system packages, network access). Most skills do not need it. Example: `Requires git, docker, and jq`. |
| `metadata`      | A string-to-string map for client-defined properties the spec does not cover, such as `author` or `version`. Use distinctive key names to avoid collisions.                 |
| `allowed-tools` | A space-separated list of pre-approved tools, for example `Bash(git:*) Read`.                                                                                               |

For the exhaustive list of frontmatter fields and their constraints, see the [Reference](./reference.md).

> [!WARNING]
> **`allowed-tools` is experimental.** Support varies between agents, and `dnx skills init` does not emit it. Treat it as a hint that some agents honor and others ignore, not a security boundary.

## The instructions body

The body is plain Markdown the agent follows when your skill activates. There are no format restrictions, so write it the way you would brief a new teammate: step-by-step instructions, input and output examples, and the edge cases people get wrong.

Keep `SKILL.md` focused. The recommendation from both [Anthropic](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills) and the open spec is to keep the body under roughly 500 lines and move long detail into supporting files (see the next section). A tight body loads fast and keeps the agent on the steps that matter.

# Add supporting files

A skill folder can hold more than `SKILL.md`. Three conventional subdirectories cover most needs.

```text
my-skill/
├── SKILL.md          # Required: frontmatter + instructions
├── references/       # Docs loaded on demand (REFERENCE.md, API.md, ...)
├── scripts/          # Code the agent runs
└── assets/           # Templates, data files, schemas
```

The CLI copies or symlinks the whole folder as a unit, so every supporting file ships with the skill. (A few build artifacts are excluded automatically, including `.git`, `node_modules`, and `__pycache__`.)

These directories exist because of [progressive disclosure](./index.md#how-skills-load-progressive-disclosure): the agent loads your skill in stages so a large skill costs almost nothing until it is needed.

- At startup the agent loads only `name` and `description`.
- When the skill triggers, the agent reads the `SKILL.md` body.
- A file under `references/`, `scripts/`, or `assets/` loads only when the body points the agent at it.

This is why moving detail out of `SKILL.md` and into `references/` is free: that material never enters the context window until the agent actually follows a link to it. Reference your supporting files with relative paths one level deep (for example `references/api.md`), and avoid deep nesting chains. For more on the three-stage model, see the [Introduction](./index.md#how-skills-load-progressive-disclosure) and [Anthropic's overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview).

# How installed skills are named

When someone installs your skill, the CLI derives the on-disk directory name by sanitizing the `name` field. It lowercases the name, replaces every run of characters outside `a-z`, `0-9`, `.`, and `_` with a single hyphen, and trims leading and trailing dots and hyphens.

For a clean lowercase-hyphen name like `release-checklist`, the on-disk name is identical, with no surprises. A name with spaces or uppercase letters gets rewritten (`My Skill` becomes `my-skill`), which can mismatch what you and your teammates expect on disk and during removal. Pick a clean lowercase-hyphen name that already satisfies the [required frontmatter rules](#required-frontmatter), and the install name matches your folder name exactly.

# Publish and install

A skill is a folder in a git repo, so publishing is committing and pushing. Any repository the CLI can reach with git works.

To publish, commit your skill folder and push it to GitHub, GitLab, or any git remote. Then anyone installs it with `dnx skills add` pointed at the repo. The forms below show what an installer runs against your published skill; for the full add workflow, including expected output, scoping, and agent targeting, see [Installing Skills](./installing-skills.md).

```shell
# install every skill in the repo
dnx skills add my-org/my-skills

# install only one skill from a multi-skill repo
dnx skills add my-org/my-skills@release-checklist

# install a single SKILL.md hosted at a URL
dnx skills add https://example.com/my-skill/SKILL.md
```

Private repositories work with no extra configuration. The CLI shells out to your own `git`, so it uses your existing credentials (SSH agent keys, a git credential helper, or `gh auth`). If you can `git clone` the repo, `skills` can install from it. When authentication fails, see [Troubleshooting](./troubleshooting.md).

To keep a work-in-progress skill out of default discovery, set `metadata.internal` to `true` in the frontmatter. The CLI hides internal skills unless the installer explicitly opts in (by filtering for the skill by name, or by setting the `INSTALL_INTERNAL_SKILLS` environment variable).

```markdown
---
name: experimental-skill
description: An in-progress skill. Use when testing new conventions before rollout.
metadata:
  internal: "true"
---
```

# Validate a skill

To check a skill against the open spec before you publish, use the reference validator `skills-ref` from the [agentskills/agentskills](https://github.com/agentskills/agentskills) repository. It validates your frontmatter and naming conventions.

```shell
skills-ref validate ./my-skill
```

It reports a clean pass when the skill is valid, or names the offending field and rule when it is not (for example, an empty `description` or a `name` that breaks the naming rules).

Running it after `dnx skills init` and again before you push catches a malformed `name` or an empty `description` while it is still fast to fix, rather than discovering that the CLI skipped your skill on install.

# Next steps

- [Installing Skills](./installing-skills.md): install your published skill, target specific agents, and choose project or global scope.
- [Reference](./reference.md): the complete command and flag surface, including `dnx skills init`.

---

### Website Content Docs Skills Installing Skills (`website/content/docs/skills/installing-skills.md`)

---
title: "Installing Skills"
description: "Install Agent Skills from GitHub repos, git URLs, or local folders with `dnx skills add`: the CLI finds every SKILL.md and wires it into your agents."
---

`dnx skills add` installs skills from a source into the AI coding agents you already have. Point it at a GitHub repository and the CLI discovers every `SKILL.md`, detects your agents, and wires the skill into each one.

```shell
dnx skills add anthropics/skills
```

That single command fetches [Anthropic's reference skills](https://github.com/anthropics/skills), then either prompts you to choose which skills and agents you want or, when it is running inside an agent, installs non-interactively. This page walks the full lifecycle: choosing a source, narrowing to specific skills and agents, picking a scope, keeping skills current, and removing them.

If you have not installed the CLI yet, start with [Getting Started](./getting-started.md).

> [!WARNING]
> **Skills run with your agent's full permissions.** A skill is executable instructions plus optional scripts that your agent runs on your machine. Install only from sources you trust, and review a skill before you use it. After every successful install, `skills` reminds you: `Review skills before use; they run with full agent permissions.`

# Choose a source

The `source` argument tells the CLI where to fetch skills from. It infers the kind of source from the string, so you pass one positional value and nothing else.

The most common form is a GitHub `owner/repo` shorthand:

```shell
dnx skills add anthropics/skills
```

Every source form below is accepted. The first matching rule wins, and clones are always shallow (the CLI fetches only the latest commit, not the full history).

| Source form            | Example                                                                      | What it does                                                              |
| ---------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| GitHub `owner/repo`    | `#!shell dnx skills add anthropics/skills`                                   | Installs from the repository root. The default shorthand.                 |
| GitHub subpath         | `#!shell dnx skills add anthropics/skills/document-skills/pdf`               | Installs only the skill at that path in the repo.                         |
| GitHub branch or tag   | `#!shell dnx skills add owner/repo#main`                                     | Installs from a specific ref (`#branch` or `#tag`).                       |
| GitHub skill filter    | `#!shell dnx skills add owner/repo@my-skill`                                 | Installs only the skill named `my-skill` from the repo.                   |
| GitHub URL             | `#!shell dnx skills add https://github.com/owner/repo`                       | Full URL, with or without a trailing `.git`.                              |
| GitHub `tree` URL      | `#!shell dnx skills add https://github.com/owner/repo/tree/main/skills/foo`  | A `/tree/<ref>/<path>` URL pins the ref and subpath.                      |
| GitLab shorthand       | `#!shell dnx skills add gitlab:group/project`                                | Installs from GitLab. Subgroups are supported (`group/subgroup/project`). |
| GitLab URL             | `#!shell dnx skills add https://gitlab.com/group/project/-/tree/main/skills` | GitLab uses `/-/tree/` (note the `-` segment).                            |
| Generic git or SSH     | `#!shell dnx skills add git@github.com:owner/repo.git`                       | Any `https`, `ssh://`, `git://`, or scp-style git transport.              |
| Local directory        | `#!shell dnx skills add ./my-skills`                                         | A local path: `./`, `../`, an absolute path, or a Windows drive path.     |
| Well-known HTTP(S) URL | `#!shell dnx skills add https://example.com/skills`                          | A non-git site that serves skills via `.well-known` discovery.            |

A few notes that save time:

- A bare name like `my-skills` (no `./` prefix) is treated as `owner/repo` shorthand, not a local path. Use `./my-skills` when you mean a local directory.
- You can combine a ref and a skill filter: `owner/repo#main@my-skill`.
- Any credentials embedded in a URL are stripped before they appear in output or in the lock file. The `Source:` line that the CLI prints shows `https://<redacted>@host/...`.

> A well-known HTTP source fetches a `.well-known/agent-skills/index.json` (or `.well-known/skills/index.json`) discovery document from the site and downloads the listed skills. Every fetch is pinned to the same origin as the index, and indexes verify a SHA-256 digest per skill.

# Choose which skills to install

A source can contain many skills. By default, when you do not pass any filter, the CLI lists what it found and lets you select interactively. To narrow the set up front, use the options below.

To preview the available skills without installing anything, pass `--list` (or `-l`):

```shell
dnx skills add anthropics/skills --list
```

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

Available Skills
  pdf
    the pdf skill
  docx
    the docx skill

Use --skill <name> to install specific skills
```

To install one or more specific skills by name, pass `--skill` (or `-s`). The option is repeatable:

```shell
dnx skills add anthropics/skills --skill pdf --skill docx
```

You can also embed a single skill filter in the source string with `@name`:

```shell
dnx skills add anthropics/skills@pdf
```

To install every skill from the source into every detected agent, non-interactively, pass `--all`:

```shell
dnx skills add anthropics/skills --all
```

A successful run prints the `Installation Summary` and `Installed N skill(s)` panels followed by the safety reminder, the same shape shown under [How skills reach each agent](#how-skills-reach-each-agent).

`--all` is a shortcut: it selects all skills, targets all agents, and skips every prompt.

If a `--skill` filter matches nothing, the CLI lists what was available so you can correct the name, and exits with a non-zero code:

```text
Source: /home/you/my-skills
Found 2 skill(s)
No matching skills found for: nope
Available skills:
  alpha
  beta
```

A skill is any folder that contains a `SKILL.md` with both a `name` and a `description`. If the source contains no valid skills, the CLI reports `No valid skills found. Skills require a SKILL.md with name and description.` and exits non-zero. See [Authoring Skills](./authoring-skills.md) for the format.

# Choose which agents

By default, the CLI detects which agents are installed on your machine and targets them. To choose explicitly, pass `--agent` (or `-a`). The option is repeatable and accepts multiple values per token, and `*` means every supported agent:

```shell
# repeated flags
dnx skills add anthropics/skills --agent claude-code --agent cursor

# space-separated values in one token
dnx skills add anthropics/skills --agent claude-code cursor

# every supported agent
dnx skills add anthropics/skills --agent "*"
```

A valid `--agent` run prints the `Installation Summary` and `Installed N skill(s)` panels shown under [How skills reach each agent](#how-skills-reach-each-agent), naming each agent that was linked or copied.

The CLI resolves the target agents like this:

- **Running inside an agent.** When the CLI runs inside an agent (for example, when your agent invokes it for you), it goes non-interactive and targets that agent plus the shared store.
- **Run from your shell, one agent installed.** The CLI auto-selects that agent plus the shared store.
- **Run from your shell, several agents installed.** The CLI prompts you to choose, pre-selecting your last-used agents (or the common defaults `claude-code`, `codex`, `opencode` on a first run).
- **`--agent` passed.** The CLI uses exactly what you specify, after validation.

These are the headline agent identifiers. Names are case-sensitive.

| Agent          | `--agent` identifier |
| -------------- | -------------------- |
| Claude Code    | `claude-code`        |
| Cursor         | `cursor`             |
| GitHub Copilot | `github-copilot`     |
| Codex          | `codex`              |
| Continue       | `continue`           |
| Gemini CLI     | `gemini-cli`         |
| Windsurf       | `windsurf`           |

The `skills` CLI supports 55+ agents. For the complete list, plus the exact directory each agent installs into, see the [Reference](./reference.md).

Because identifiers are case-sensitive, `copilot` and `Claude-Code` are not valid. An invalid value fails fast and lists every valid name so you can copy the right one:

```text
┌─Invalid agents───────────────────────────────────────────────────────────────┐
│ Invalid agents: bogus                                                        │
│                                                                              │
│ Valid agents: adal, aider-desk, amp, antigravity, augment, bob, claude-code, │
│ cline, codearts-agent, codebuddy, codemaker, codestudio, codex,              │
│ command-code, continue, cortex, crush, cursor, deepagents, devin, dexto,     │
│ droid, firebender, forgecode, gemini-cli, github-copilot, goose,             │
│ hermes-agent, iflow-cli, junie, kilo, kimi-cli, kiro-cli, kode, mcpjam,      │
│ mistral-vibe, mux, neovate, openclaw, opencode, openhands, pi, pochi, qoder, │
│ qwen-code, replit, roo, rovodev, tabnine-cli, trae, trae-cn, universal,      │
│ warp, windsurf, zencoder                                                     │
└──────────────────────────────────────────────────────────────────────────────┘
```

# Project and global scope

The CLI installs into one of two scopes. Understanding the difference tells you where skills live and who shares them.

**Project scope is the default.** The CLI records the installed skills in a `skills-lock.json` file in your working directory and materializes the skill folders under `./.agents/skills`. Commit `skills-lock.json` so everyone who clones the repository shares the same skill set. This is the right scope for skills your whole team should use on a given project.

**Global scope (`--global` or `-g`) installs skills for your user account**, across every project.

```shell
dnx skills add anthropics/skills --global
```

In global scope the lock file lives at `~/.local/share/skills/.skill-lock.json` (honoring `XDG_DATA_HOME` if you set it), and the skill folders are materialized under `~/.agents/skills`.

In both scopes, the CLI materializes each skill once in a canonical store, then makes each targeted agent point at it.

# How skills reach each agent

By default, the CLI writes each skill once into the canonical store and creates a symlink from every targeted agent's directory back to it. Editing the one canonical copy updates the skill for every agent at once, so you maintain a single source of truth.

```mermaid
flowchart LR
  S["Source (repo, URL, or local path)"] --> C["Canonical store (.agents/skills)"]
  C -. "symlink" .-> A1[".claude/skills"]
  C -. "symlink" .-> A2[".windsurf/skills"]
  C --> A3["Universal agents (.agents/skills is the store)"]
```

Some agents read directly from the shared `.agents/skills` store, so their skills directory _is_ that store and no symlink is needed. The [Reference](./reference.md) lists which agents read from the shared store and the directory each agent installs into.

To copy files instead of symlinking, pass `--copy`. Use it for sandboxed agents that cannot follow symlinks (for example, agents running in containers where symlinks across mounts break):

```shell
dnx skills add anthropics/skills --copy --agent claude-code --agent windsurf
```

The CLI also copies automatically in two cases, so you rarely need `--copy` by hand:

- When a symlink cannot be created (for example, on Windows without the symlink privilege), the CLI falls back to copying for that agent.
- When every targeted agent shares one skills directory, the CLI copies, because symlinking only makes sense across distinct directories.

A successful install reports which agents were linked or copied and where each skill landed:

```text
Source: https://github.com/owner/skills.git
Found 1 skill(s)

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

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

If everything worked, you should see the `Installation Summary` panel followed by an `Installed N skill(s)` panel and the safety reminder. To confirm later, run `dnx skills list` (see the [Reference](./reference.md)).

# Scan nested skills with --full-depth

By default, the CLI installs the skill at the source root if one exists there. If the root is not itself a skill, it discovers every nested skill instead. That fast path covers most repositories.

To find skills in nested and curated directories that the default scan does not reach, pass `--full-depth`:

```shell
dnx skills add owner/curated-skills --full-depth
```

The install reports the same `Installation Summary` and `Installed N skill(s)` panels shown under [How skills reach each agent](#how-skills-reach-each-agent); only the set of discovered skills differs.

`--full-depth` widens _discovery_: it scans nested directories and curated locations (such as `skills/.curated`) so the CLI finds skills the default scan would skip. It does not change clone depth. Clones are always shallow regardless of this flag.

# Install from a private repository

The CLI never manages or prompts for credentials. It shells out to `git`, which uses your own setup: SSH agent keys, a git credential helper, `gh` authentication, or `~/.netrc`. To avoid hanging on a missing credential, it sets `GIT_TERMINAL_PROMPT=0`, so an unauthenticated clone fails fast with a clear error instead of waiting at a prompt.

To install from a private repository, make sure your normal git auth already works, then run `add` as usual:

```shell
dnx skills add owner/private-skills --agent claude-code
```

This prints the same `Installation Summary` and `Installed N skill(s)` panels shown under [How skills reach each agent](#how-skills-reach-each-agent), once the clone authenticates.

Before you install, verify your access with one of:

```shell
ssh -T git@github.com   # confirm your SSH key reaches GitHub
gh auth login           # or authenticate the GitHub CLI
```

A working SSH key prints the well-known success line (GitHub closes the connection because it does not provide shell access):

```text
Hi <user>! You've successfully authenticated, but GitHub does not provide shell access.
```

If a clone fails with an authentication error, the CLI prints guidance pointing you back to your git credentials. See [Troubleshooting](./troubleshooting.md) for the full list of clone and auth failures.

# Keep skills up to date

`dnx skills update` checks your installed skills for newer versions. It has the aliases `upgrade` and `check`.

```shell
dnx skills update -g
```

```text
Checking for skill updates...

Checking global skill 1/1: my-skill
Found 1 global update(s)

Update available: my-skill
  Run: skills add owner/repo/skills/my-skill -g -y

Updates available for 1 skill(s); no updates were applied.
```

**`update` only reports; it never applies changes.** It prints the exact `skills add ... -y` command to run for each skill that has an update (prefix it with `dnx` when you run through `dnx`). Copy that command and run it yourself to apply the update. The output is explicit: `no updates were applied.`

Scope the check the same way you scope an install:

- `-g` (or `--global`) checks global skills only.
- `-p` (or `--project`) checks project skills only.
- Pass both, or name specific skills, to check both scopes.
- With no scope flag in an interactive shell, the CLI prompts you to choose. Non-interactively (for example with `-y`), it checks both scopes.

When there is nothing to update, you get a clean report:

```text
Checking for skill updates...

Checking global skill 1/1: my-skill
All global skills are up to date
```

The update check calls the GitHub API. If you hit a rate limit, set a `GITHUB_TOKEN` or `GH_TOKEN` environment variable (or sign in with `gh auth login`) and the CLI uses it for the check. See [Troubleshooting](./troubleshooting.md) for rate-limit and timeout details.

# Remove skills

`dnx skills remove` uninstalls skills. It unlinks each agent's copy and removes the canonical store entry once no remaining agent references it, then updates the lock file.

To remove specific skills by name, list them:

```shell
dnx skills remove alpha --yes
```

```text
Successfully removed 1 skill(s)
```

To remove everything, pass `--all`:

```shell
dnx skills remove --all
```

```text
Successfully removed 1 skill(s)
```

To limit removal to specific agents, add `--agent`. To target your user-level skills, add `--global`.

Run `dnx skills remove` with no names to remove interactively: the CLI shows a multi-select prompt, then a confirmation that defaults to **no**. Declining cancels and removes nothing:

```text
$ dnx skills remove

Removal cancelled
```

If a named skill is not installed, the CLI tells you and exits cleanly without changing anything:

```text
No matching skills found for: nope
```

# Next steps

- [Authoring Skills](./authoring-skills.md): write and publish your own `SKILL.md` with `dnx skills init`.
- [Reference](./reference.md): every command, flag, the full list of 55+ agents, and per-agent install directories.
- [Troubleshooting](./troubleshooting.md): clone failures, authentication errors, rate limits, and symlink fallbacks.

---

### Website Content Docs Skills Reference (`website/content/docs/skills/reference.md`)

---
title: "Reference"
description: "Complete `skills` CLI reference covering every command and flag, JSON output, exit codes, environment variables, file locations, and supported agents."
---

This page is the complete, precise reference for the `skills` command surface, JSON output, exit codes, environment variables, file locations, and supported agents. It is meant for lookup, not for learning. To learn by doing, start at [Installing Skills](./installing-skills.md) and [Authoring Skills](./authoring-skills.md).

Every command prints the same per-command help at the terminal. Run `dnx skills <command> --help` to see the synopsis, arguments, and options for that command.

```shell
dnx skills add --help
```

```text
Description:
  Add a skill from a source

Usage:
  skills add [<source>] [options]

Arguments:
  <source>  Source to fetch skills from (e.g., owner/repo, URL, local path)

Options:
  -g, --global    Install globally
  -a, --agent     Target agent(s)
  -s, --skill     Skill name filter(s)
  -y, --yes       Skip prompts (non-interactive)
  --all           Install all skills to all agents
  --copy          Copy instead of symlinking
  --full-depth    Scan nested directories for skills
  -l, --list      List available skills without installing
  -?, -h, --help  Show help and usage information
```

> The binary on your PATH is `skills`. Install it with `#!shell dotnet tool install -g skills`, or run it without installing via `dnx skills <command>` (requires the .NET 10 SDK or later). Both forms accept the same commands, arguments, and options documented below.

# Commands

`skills` has five commands: `add`, `remove`, `list`, `update`, and `init`. Each command's options are independent. There are no shared global options beyond `--help` and `--version` (see [Global flags](#global-flags)).

## skills add &lt;source&gt;

Add skills from a source. Skills are discovered at the source, then materialized into a canonical store and linked into each target agent's skills directory.

```text
dnx skills add <source> [options]
```

### Arguments

| Argument | Arity | Required | Description                                                                                                                                                        |
| -------- | ----- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `source` | one   | yes      | Source to fetch skills from (for example `owner/repo`, a full git URL, or a local path). If omitted, the command exits 1 with `Missing required argument: source`. |

### Options

| Option         | Alias  | Type                | Default         | Description                                                                                                                                                                                         |
| -------------- | ------ | ------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--global`     | `-g`   | bool                | `false`         | Install to the global scope (your home directory) instead of the current project.                                                                                                                   |
| `--agent`      | `-a`   | string (repeatable) | (auto-detected) | Target agent(s). Repeatable and space-separated (`--agent claude-code cursor` or `--agent claude-code --agent cursor`). `--agent *` targets every agent. See [Supported agents](#supported-agents). |
| `--skill`      | `-s`   | string (repeatable) | (all)           | Install only skills whose name matches the given filter(s). Repeatable.                                                                                                                             |
| `--yes`        | `-y`   | bool                | `false`         | Skip all prompts and run non-interactively.                                                                                                                                                         |
| `--all`        | (none) | bool                | `false`         | Install all skills to all agents. Implies `--skill *`, `--agent *`, and `--yes`.                                                                                                                    |
| `--copy`       | (none) | bool                | `false`         | Copy skill files into each agent directory instead of symlinking. Use for agents that do not follow symlinks.                                                                                       |
| `--full-depth` | (none) | bool                | `false`         | Widen discovery to scan nested directories in the source, not only the source root. This does not change the git clone (clones are always shallow).                                                 |
| `--list`       | `-l`   | bool                | `false`         | List the available skills at the source without installing anything, then exit.                                                                                                                     |

The default install mode is symlink: the CLI materializes each skill once into a canonical store and creates a relative symlink from each agent directory back to it. Pass `--copy` to copy instead. When every selected agent shares one skills directory, copy is used automatically.

For sources, scopes, and install mechanics, see [Installing Skills](./installing-skills.md). For agent targeting, see [Supported agents](#supported-agents).

## skills remove [skills...]

Remove installed skills. With no skill names and an interactive terminal, the CLI prompts you to select which skills to remove.

```text
dnx skills remove [skills...] [options]
```

### Arguments

| Argument | Arity        | Required | Description                                                                                          |
| -------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------- |
| `skills` | zero or more | no       | Skill names to remove. Matched case-insensitively. With no names and no `--all`, runs interactively. |

### Options

| Option     | Alias  | Type                | Default | Description                                                  |
| ---------- | ------ | ------------------- | ------- | ------------------------------------------------------------ |
| `--global` | `-g`   | bool                | `false` | Remove from the global scope instead of the current project. |
| `--agent`  | `-a`   | string (repeatable) | (all)   | Limit removal to specific agent(s). Repeatable.              |
| `--yes`    | `-y`   | bool                | `false` | Skip prompts and run non-interactively.                      |
| `--all`    | (none) | bool                | `false` | Remove every installed skill.                                |

In interactive mode, the CLI shows a multiselect prompt followed by a confirmation prompt. The confirmation defaults to no. Declining the confirmation prints `Removal cancelled` and exits with code 130 (see [Exit codes](#exit-codes)).

```text
$ dnx skills remove alpha
# exit 130

Removal cancelled
```

## skills list

List installed skills. With no options, lists the current project's skills.

```text
dnx skills list [options]
```

This command takes no positional arguments.

### Options

| Option     | Alias  | Type                | Default | Description                                                                           |
| ---------- | ------ | ------------------- | ------- | ------------------------------------------------------------------------------------- |
| `--global` | `-g`   | bool                | `false` | List skills in the global scope instead of the current project.                       |
| `--agent`  | `-a`   | string (repeatable) | (none)  | Filter the listing to specific agent(s). Repeatable.                                  |
| `--format` | (none) | `text` \| `json`    | `text`  | Output format. `json` emits a JSON array to stdout (see [JSON output](#json-output)). |
| `--json`   | (none) | bool                | `false` | Shorthand for `--format json`.                                                        |

JSON output is enabled when either `--json` is present or `--format json` is set (case-insensitive). Enabling JSON suppresses the banner and writes the array to stdout.

## skills update [skills...]

Check for available updates and print the exact command to apply each one.

```text
dnx skills update [skills...] [options]
```

> Aliases: `upgrade` and `check`. `dnx skills upgrade` and `dnx skills check` are identical to `dnx skills update`.

> [!WARNING]
> **`update` only reports.** It never modifies files or lock files. For each skill that has an update, it prints a `skills add ...` command you can copy and run to apply it (run it as `dnx skills add ...` if you use `dnx`). The command always exits 0, even when updates are available. Its output ends with `no updates were applied.`

### Arguments

| Argument | Arity        | Required | Description                                                                                                        |
| -------- | ------------ | -------- | ------------------------------------------------------------------------------------------------------------------ |
| `skills` | zero or more | no       | Optional skill names to check. Scope is resolved the same way with or without names (see the scope options below). |

### Options

| Option      | Alias | Type | Default | Description                                                                |
| ----------- | ----- | ---- | ------- | -------------------------------------------------------------------------- |
| `--global`  | `-g`  | bool | `false` | Check global skills only.                                                  |
| `--project` | `-p`  | bool | `false` | Check project skills only.                                                 |
| `--yes`     | `-y`  | bool | `false` | Skip the interactive scope prompt. Non-interactive runs check both scopes. |

With neither `-g` nor `-p`, an interactive terminal prompts you to choose Project, Global, or Both. Passing both flags, or running non-interactively, checks both scopes.

```text
$ dnx skills update -g

Checking for skill updates...

Checking global skill 1/1: my-skill
Found 1 global update(s)

Update available: my-skill
  Run: skills add owner/repo/skills/my-skill -g -y

Updates available for 1 skill(s); no updates were applied.
```

## skills init [name]

Scaffold a new skill directory containing a `SKILL.md` template. This command has no options.

```text
dnx skills init [name]
```

### Arguments

| Argument | Arity       | Required | Description                                                                                                                                                       |
| -------- | ----------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`   | zero or one | no       | Skill name. Creates `<name>/SKILL.md`. The name is sanitized to a slug. With no name, derives the slug from the current directory and writes `SKILL.md` in place. |

If the target `SKILL.md` already exists, the CLI does not overwrite it. It prints `Skill already exists at <path>` and exits 0.

```text
$ dnx skills init my-skill

Initialized skill: my-skill

Created:
  my-skill/SKILL.md

Next steps:
  1. Edit my-skill/SKILL.md to define your skill instructions
  2. Update the name and description in the frontmatter

Publishing:
  GitHub: Push to a repo, then skills add <owner>/<repo>
  URL:    Host the file, then skills add https://example.com/my-skill/SKILL.md
```

See [Authoring Skills](./authoring-skills.md) for the `SKILL.md` format and publishing workflow.

## Global flags

These are provided on the root command and on every subcommand.

| Option      | Aliases    | Type | Description                                            |
| ----------- | ---------- | ---- | ------------------------------------------------------ |
| `--help`    | `-h`, `-?` | bool | Show help. On a subcommand, shows that command's help. |
| `--version` | (none)     | bool | Print the installed CLI version and exit 0.            |

Running `skills` with no arguments shows the banner and exits 0. Running `#!shell dnx skills --help` shows curated top-level help. A bare `--` token is removed before parsing, so `#!shell dnx skills add --agent codex -- owner/repo` parses the same as without it.

# JSON output

`#!shell dnx skills list --json` (equivalently `#!shell dnx skills list --format json`) prints a JSON array to stdout. Each element describes one installed skill.

```shell
dnx skills list --json
```

```json
[
  {
    "name": "alpha",
    "path": "/home/you/project/.agents/skills/alpha",
    "scope": "project",
    "agents": ["Claude Code", "Windsurf"]
  }
]
```

| Field    | Type     | Description                                                                                                                |
| -------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| `name`   | string   | The skill's sanitized name (its on-disk directory name).                                                                   |
| `path`   | string   | Absolute path to the skill in the canonical store.                                                                         |
| `scope`  | string   | `"project"` or `"global"`. `"global"` when listed with `-g`, otherwise `"project"`.                                        |
| `agents` | string[] | Display names of agents linked to this skill (for example `Claude Code`). Empty when the skill is not linked to any agent. |

The array is written to stdout, so you can pipe it to a tool such as `jq`. Banner and progress output are suppressed in JSON mode.

# Exit codes

`skills` uses exactly three exit codes.

| Code  | Name      | Meaning                                                                                                                                        |
| ----- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `0`   | Success   | The command completed. Note that `update` exits 0 even when updates are available, and `remove` exits 0 when there is nothing to remove.       |
| `1`   | Failure   | The command failed (for example an invalid agent name, no valid skills found, or a file system error). The error message is written to stderr. |
| `130` | Cancelled | The operation was cancelled, either by declining an interactive confirmation or by pressing Ctrl+C.                                            |

# Environment variables

| Variable                  | Default          | Effect                                                                                                                                                            |
| ------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SKILLS_CLONE_TIMEOUT_MS` | `300000`         | Git clone timeout in milliseconds (5 minutes). Must parse to a positive integer, otherwise the default is used. Raise it for large repositories or slow networks. |
| `GITHUB_TOKEN`            | (unset)          | A GitHub API token. Used only by `update` to raise GitHub API rate limits when checking for updates. Not used for cloning.                                        |
| `GH_TOKEN`                | (unset)          | Fallback GitHub API token. Consulted by `update` after `GITHUB_TOKEN`. Same effect.                                                                               |
| `INSTALL_INTERNAL_SKILLS` | (unset)          | Set to `1` or `true` to include skills their authors marked as internal in discovery. By default such skills are hidden.                                          |
| `CLAUDE_CONFIG_DIR`       | `~/.claude`      | Overrides the Claude Code configuration directory used for detection and global installs.                                                                         |
| `CODEX_HOME`              | `~/.codex`       | Overrides the Codex configuration directory used for detection and global installs.                                                                               |
| `VIBE_HOME`               | `~/.vibe`        | Overrides the Mistral Vibe configuration directory used for detection and global installs.                                                                        |
| `XDG_DATA_HOME`           | `~/.local/share` | Root for the global lock location. The global lock lives at `$XDG_DATA_HOME/skills/.skill-lock.json`.                                                             |

`XDG_CONFIG_HOME` (default `~/.config`) and `XDG_STATE_HOME` (default `~/.local/state`) affect where the CLI keeps its own configuration and logs, and where several agents resolve their global directories.

For private repositories, the CLI shells out to `git` and uses your existing git credentials (SSH agent, credential helper, `gh` auth). See [Troubleshooting](./troubleshooting.md) for authentication and rate-limit guidance.

# File locations

The CLI writes a lock file per scope and materializes skills into a canonical store. Project scope is the current working directory; global scope is your home directory.

| Scope   | Lock file                                                                              | Canonical skills store |
| ------- | -------------------------------------------------------------------------------------- | ---------------------- |
| Project | `skills-lock.json` (in the current working directory)                                  | `./.agents/skills`     |
| Global  | `~/.local/share/skills/.skill-lock.json` (or `$XDG_DATA_HOME/skills/.skill-lock.json`) | `~/.agents/skills`     |

Non-universal agents receive a symlink from their own directory (for example `.claude/skills/<name>`) back into the canonical store. Universal agents use the canonical store directly. See [Supported agents](#supported-agents) for per-agent directories.

# Supported agents

The `skills` CLI supports 55+ agents. It detects which agents are installed on your machine by probing each agent's configuration directory, and it detects which agent it is running inside by reading environment variables that agent hosts set. When run inside a detected agent, `add` runs non-interactively and targets that agent plus the universal agents.

Agent identifiers are case-sensitive (the identifier is `github-copilot`, not `copilot`). The `--agent` option is repeatable and accepts multiple values per token. `--agent *` targets all agents. "Universal" agents share the `.agents/skills` store, so installing a skill for one universal agent makes it visible to all of them.

In the table below, project directories are relative to the working directory and global directories are absolute (`~` is your home directory, `<config>` is `$XDG_CONFIG_HOME` or `~/.config`). Agents marked "universal" use the shared `.agents/skills` store at project scope.

| Identifier       | Display name       | Project directory            | Global directory                             |
| ---------------- | ------------------ | ---------------------------- | -------------------------------------------- |
| `adal`           | AdaL               | `.adal/skills`               | `~/.adal/skills`                             |
| `aider-desk`     | AiderDesk          | `.aider-desk/skills`         | `~/.aider-desk/skills`                       |
| `amp`            | Amp                | `.agents/skills` (universal) | `<config>/agents/skills`                     |
| `antigravity`    | Antigravity        | `.agents/skills` (universal) | `~/.gemini/antigravity/skills`               |
| `augment`        | Augment            | `.augment/skills`            | `~/.augment/skills`                          |
| `bob`            | IBM Bob            | `.bob/skills`                | `~/.bob/skills`                              |
| `claude-code`    | Claude Code        | `.claude/skills`             | `~/.claude/skills` (via `CLAUDE_CONFIG_DIR`) |
| `cline`          | Cline              | `.agents/skills` (universal) | `~/.agents/skills`                           |
| `codearts-agent` | CodeArts Agent     | `.codeartsdoer/skills`       | `~/.codeartsdoer/skills`                     |
| `codebuddy`      | CodeBuddy          | `.codebuddy/skills`          | `~/.codebuddy/skills`                        |
| `codemaker`      | Codemaker          | `.codemaker/skills`          | `~/.codemaker/skills`                        |
| `codestudio`     | Code Studio        | `.codestudio/skills`         | `~/.codestudio/skills`                       |
| `codex`          | Codex              | `.agents/skills` (universal) | `~/.codex/skills` (via `CODEX_HOME`)         |
| `command-code`   | Command Code       | `.commandcode/skills`        | `~/.commandcode/skills`                      |
| `continue`       | Continue           | `.continue/skills`           | `~/.continue/skills`                         |
| `cortex`         | Cortex Code        | `.cortex/skills`             | `~/.snowflake/cortex/skills`                 |
| `crush`          | Crush              | `.crush/skills`              | `~/.config/crush/skills`                     |
| `cursor`         | Cursor             | `.agents/skills` (universal) | `~/.cursor/skills`                           |
| `deepagents`     | Deep Agents        | `.agents/skills` (universal) | `~/.deepagents/agent/skills`                 |
| `devin`          | Devin for Terminal | `.devin/skills`              | `<config>/devin/skills`                      |
| `dexto`          | Dexto              | `.agents/skills` (universal) | `~/.agents/skills`                           |
| `droid`          | Droid              | `.factory/skills`            | `~/.factory/skills`                          |
| `firebender`     | Firebender         | `.agents/skills` (universal) | `~/.firebender/skills`                       |
| `forgecode`      | ForgeCode          | `.forge/skills`              | `~/.forge/skills`                            |
| `gemini-cli`     | Gemini CLI         | `.agents/skills` (universal) | `~/.gemini/skills`                           |
| `github-copilot` | GitHub Copilot     | `.agents/skills` (universal) | `~/.copilot/skills`                          |
| `goose`          | Goose              | `.goose/skills`              | `<config>/goose/skills`                      |
| `hermes-agent`   | Hermes Agent       | `.hermes/skills`             | `~/.hermes/skills`                           |
| `iflow-cli`      | iFlow CLI          | `.iflow/skills`              | `~/.iflow/skills`                            |
| `junie`          | Junie              | `.junie/skills`              | `~/.junie/skills`                            |
| `kilo`           | Kilo Code          | `.kilocode/skills`           | `~/.kilocode/skills`                         |
| `kimi-cli`       | Kimi Code CLI      | `.agents/skills` (universal) | `~/.config/agents/skills`                    |
| `kiro-cli`       | Kiro CLI           | `.kiro/skills`               | `~/.kiro/skills`                             |
| `kode`           | Kode               | `.kode/skills`               | `~/.kode/skills`                             |
| `mcpjam`         | MCPJam             | `.mcpjam/skills`             | `~/.mcpjam/skills`                           |
| `mistral-vibe`   | Mistral Vibe       | `.vibe/skills`               | `~/.vibe/skills` (via `VIBE_HOME`)           |
| `mux`            | Mux                | `.mux/skills`                | `~/.mux/skills`                              |
| `neovate`        | Neovate            | `.neovate/skills`            | `~/.neovate/skills`                          |
| `openclaw`       | OpenClaw           | `skills`                     | `~/.openclaw/skills`                         |
| `opencode`       | OpenCode           | `.agents/skills` (universal) | `<config>/opencode/skills`                   |
| `openhands`      | OpenHands          | `.openhands/skills`          | `~/.openhands/skills`                        |
| `pi`             | Pi                 | `.pi/skills`                 | `~/.pi/agent/skills`                         |
| `pochi`          | Pochi              | `.pochi/skills`              | `~/.pochi/skills`                            |
| `qoder`          | Qoder              | `.qoder/skills`              | `~/.qoder/skills`                            |
| `qwen-code`      | Qwen Code          | `.qwen/skills`               | `~/.qwen/skills`                             |
| `replit`         | Replit             | `.agents/skills` (universal) | `<config>/agents/skills`                     |
| `roo`            | Roo Code           | `.roo/skills`                | `~/.roo/skills`                              |
| `rovodev`        | Rovo Dev           | `.rovodev/skills`            | `~/.rovodev/skills`                          |
| `tabnine-cli`    | Tabnine CLI        | `.tabnine/agent/skills`      | `~/.tabnine/agent/skills`                    |
| `trae`           | Trae               | `.trae/skills`               | `~/.trae/skills`                             |
| `trae-cn`        | Trae CN            | `.trae/skills`               | `~/.trae-cn/skills`                          |
| `universal`      | Universal          | `.agents/skills` (universal) | `<config>/agents/skills`                     |
| `warp`           | Warp               | `.agents/skills` (universal) | `~/.agents/skills`                           |
| `windsurf`       | Windsurf           | `.windsurf/skills`           | `~/.codeium/windsurf/skills`                 |
| `zencoder`       | Zencoder           | `.zencoder/skills`           | `~/.zencoder/skills`                         |

Passing an unknown identifier exits 1 and prints the full list of valid identifiers:

```text
$ dnx skills add ./local-path --yes --agent bogus
# exit 1

Invalid agents: bogus
```

# See also

- [Installing Skills](./installing-skills.md): sources, scopes, and the install workflow in depth.
- [Authoring Skills](./authoring-skills.md): the `SKILL.md` format, `dnx skills init`, and publishing.
- [Troubleshooting](./troubleshooting.md): authentication, rate limits, timeouts, and common errors.

---

### Website Content Docs Skills Troubleshooting (`website/content/docs/skills/troubleshooting.md`)

---
title: "Troubleshooting"
description: "Troubleshoot `skills` CLI errors: each section maps the exact message the CLI prints to its cause and a copy-pasteable fix for installing and updating skills."
---

This page maps the errors and surprises you hit when running `skills` to their cause and fix. Each section starts with the exact message the CLI prints (where one exists), explains why it happens, and gives you a copy-pasteable way out.

If you are new to `skills`, start with [Getting Started](./getting-started.md). For the full command and flag surface, see the [Reference](./reference.md). For what each agent identifier means and where skills land, see [Installing Skills](./installing-skills.md).

# "No valid skills found. Skills require a SKILL.md with name and description."

You ran `dnx skills add <source>` and the CLI reached the source but found nothing to install:

```text
$ dnx skills add ./my-skills --yes --agent claude-code
# exit 1

Source: /home/you/my-skills
No valid skills found. Skills require a SKILL.md with name and description.
```

Cause: a skill is a directory that contains a `SKILL.md` file with both `name` and `description` in its YAML frontmatter. The CLI silently skips any folder that is missing the file, missing either field, or whose `name`/`description` collapses to empty. By default, it only looks at the source root (or the subpath you pointed it at), so a `SKILL.md` buried in a nested directory is not discovered.

To see exactly what the CLI finds without installing anything, run `--list`:

```shell
dnx skills add owner/repo --list
```

```text
Source: https://github.com/owner/repo.git
Found 2 skill(s)

Available Skills
  alpha
    the alpha skill
  beta
    the beta skill

Use --skill <name> to install specific skills
```

If `--list` reports `Found 0 skill(s)`, work through these in order:

| Check                                                     | Fix                                                                                                                            |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Is your path or subpath pointing at the right folder?     | Point at the directory that contains `SKILL.md`, or add the repo subpath: `#!shell dnx skills add owner/repo/skills/my-skill`. |
| Does the `SKILL.md` have both `name:` and `description:`? | Add the missing field. Both must be non-empty. See [Authoring Skills](./authoring-skills.md).                                  |
| Are the skills in nested directories?                     | Add `--full-depth` to scan nested directories: `#!shell dnx skills add owner/repo --full-depth --list`.                        |

> `--full-depth` widens discovery so the CLI scans nested directories for skills. It does not change how much of the repository is cloned. Clones are always shallow regardless of this flag.

# "Invalid agents: &lt;name&gt;"

You passed an `--agent` value that the CLI does not recognize:

```text
$ dnx skills add ./my-skills --yes --agent bogus
# exit 1

Source: /home/you/my-skills
Found 1 skill(s)

┌─Invalid agents───────────────────────────────────────────────────────────────┐
│ Invalid agents: bogus                                                        │
│                                                                              │
│ Valid agents: adal, aider-desk, amp, antigravity, augment, bob, claude-code, │
│ cline, codearts-agent, codebuddy, codemaker, codestudio, codex,              │
│ command-code, continue, cortex, crush, cursor, deepagents, devin, dexto,     │
│ droid, firebender, forgecode, gemini-cli, github-copilot, goose,             │
│ hermes-agent, iflow-cli, junie, kilo, kimi-cli, kiro-cli, kode, mcpjam,      │
│ mistral-vibe, mux, neovate, openclaw, opencode, openhands, pi, pochi, qoder, │
│ qwen-code, replit, roo, rovodev, tabnine-cli, trae, trae-cn, universal,      │
│ warp, windsurf, zencoder                                                     │
└──────────────────────────────────────────────────────────────────────────────┘
```

Cause: the value is misspelled or uses the wrong case. Agent identifiers are case-sensitive, and several differ from the product's marketing name. The most common mistakes:

| You typed                   | Use instead      |
| --------------------------- | ---------------- |
| `copilot`, `github_copilot` | `github-copilot` |
| `Claude-Code`, `claude`     | `claude-code`    |
| `gemini`                    | `gemini-cli`     |
| `roo-code`                  | `roo`            |

The error panel always prints every valid identifier, so copy the correct one from the list. The same `Invalid agents: <name>` message appears (without the panel) from `dnx skills list` and `dnx skills remove`. For the full table of identifiers and where each one installs skills, see [Installing Skills](./installing-skills.md) and the [Reference](./reference.md).

# Cloning a private repository fails with "Authentication failed"

`dnx skills add` reports an authentication error and stops before installing.

Cause: the CLI never prompts for credentials. It shells out to your own `git`, which uses your existing setup (SSH agent keys, a git credential helper, `gh` auth, or `~/.netrc`). When git cannot authenticate, the CLI surfaces the failure instead of hanging. Any credentials embedded in a URL are redacted from the output, so secrets never appear in errors or in the lock file.

To fix it, confirm your machine can reach the repository, then retry the same `dnx skills add` command.

For SSH sources (`git@github.com:owner/repo.git`):

```shell
ssh -T git@github.com
# Hi <you>! You've successfully authenticated, but GitHub does not provide shell access.
```

For HTTPS sources, authenticate with the GitHub CLI or configure a git credential helper. These are one-time setup commands whose output varies by environment:

```shell
gh auth login                         # uses the gh credential helper
# or
git config --global credential.helper store
```

After `gh auth login` completes, it confirms the authenticated account.

Once `git clone <repo>` succeeds on its own, `dnx skills add <repo>` will succeed too.

# The clone times out

A large repository can exceed the default clone budget and cause the CLI to abort the fetch.

Cause: the CLI caps each clone at 5 minutes (300000 ms). The clone is always shallow (latest commit only), but a very large repository or a slow connection can still run past the limit.

To raise the timeout, set `SKILLS_CLONE_TIMEOUT_MS` in milliseconds before running the command:

```shell
SKILLS_CLONE_TIMEOUT_MS=600000 dnx skills add owner/big-repo --agent claude-code   # 10 minutes
```

With the longer budget the clone finishes and the install completes with the usual `Installed 1 skill(s)` panel.

Alternatively, clone the repository yourself and point `skills` at the local copy. This skips the network step entirely:

```shell
git clone --depth 1 https://github.com/owner/big-repo.git
dnx skills add ./big-repo --agent claude-code
```

Pointing `skills` at the local clone installs the skill without touching the network and prints the same `Installed 1 skill(s)` panel.

# A skill does not appear for one agent

`dnx skills add` reports success, the skill shows up in `dnx skills list`, but one of your agents does not see it.

Cause: agents that keep skills in their own directory (for example `.claude/skills` for Claude Code or `.windsurf/skills` for Windsurf) only get a link when that agent's directory already exists in your project. The CLI will not create a config directory for an agent you do not use. When the directory is missing, it skips the link for that agent but still materializes the skill in the shared `.agents/skills` store, so the skill is installed; it is only the per-agent link that is missing.

Pick whichever fix matches your intent:

| Goal                                                           | Fix                                                                                                                                   |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| You do use that agent in this project                          | Create its directory, then re-run the install: `#!shell mkdir -p .claude && dnx skills add owner/repo --agent claude-code`.           |
| You want a self-contained copy regardless of agent directories | Add `--copy` to write the skill straight into each agent's directory: `#!shell dnx skills add owner/repo --agent claude-code --copy`. |
| You meant a different agent                                    | Re-run with the correct `--agent` value. See [Installing Skills](./installing-skills.md).                                             |

After the directory exists, re-running the install links the skill into `.claude/skills`. The Installation Summary then lists `Symlinked:  Claude Code` instead of skipping that agent.

Agents that share the universal `.agents/skills` store (Cursor, Codex, GitHub Copilot, Gemini CLI, and others) always see the skill because their directory is the canonical store.

# Symlinks are not created (Windows)

On Windows, `skills` copies skills into agent directories instead of linking them.

Cause: creating a symlink on Windows requires either an elevated process or Developer Mode. When the CLI cannot create the link, it automatically falls back to copying the skill so the install still succeeds. The skill works either way; the only difference is that copies do not share a single editable source of truth the way symlinks do.

To get symlinks instead of copies, choose one:

- Enable Developer Mode (Settings, "For developers", "Developer Mode"), then re-run the command.
- Run your terminal elevated ("Run as administrator") and re-run the command.

If you prefer copies and want the CLI to copy without attempting a symlink first, pass `--copy` explicitly:

```shell
dnx skills add owner/repo --agent claude-code --copy
```

The output shows the skill was copied rather than linked: the Installation Summary lists `Copied:  Claude Code`, and the skill is written directly into `.claude/skills`.

# "skills update" cannot check a skill or hits a rate limit

`dnx skills update` reports that one or more skills could not be checked:

```text
$ dnx skills update -g

Checking for skill updates...


1 skill(s) cannot be checked automatically:
  * legacy (Private or deleted repo)
    To update: skills add https://github.com/owner/repo -g -y
```

Cause: `update` checks for newer versions by calling the GitHub API, and unauthenticated calls are rate-limited. It also cannot diff every kind of source. Skills installed from a local path, a generic git URL, a well-known HTTP source, or a private or deleted repository have no checkable remote tree, so they are reported as "cannot be checked automatically" with a manual refresh hint.

To raise the rate limit, give `skills` a GitHub token and re-run. The CLI reads `GITHUB_TOKEN`, then `GH_TOKEN`, then falls back to `gh auth token`:

```shell
export GITHUB_TOKEN=ghp_...   # or: gh auth login
dnx skills update
```

With the token in place the rate limit clears and the check completes:

```text
Checking for skill updates...

All skills are up to date.
```

For sources that can never be checked automatically, re-install with `dnx skills add` to refresh them. The `update` output prints the exact command to run for each one:

```shell
dnx skills add owner/repo/skills/my-skill -y
```

The refresh re-installs the skill from its source and prints the usual `Installed 1 skill(s)` panel.

> `dnx skills update` only checks for and reports available updates. It never applies them. Whatever it finds, it prints the precise `skills add ...` command for you to run (prefix it with `dnx` when you use `dnx`). The aliases `dnx skills upgrade` and `dnx skills check` behave identically. See the [Reference](./reference.md).

# "... is newer than this skills supports"

A command refuses to write to a lock file and reports that the on-disk version is newer than this version of `skills` supports.

Cause: the lock file (`skills-lock.json` in your project, or the global `.skill-lock.json`) was written by a newer version of `skills` than the one you are running. The CLI reads newer lock files but refuses to modify them, so it does not corrupt state written by a version it does not understand.

To fix it, update `skills` to the latest release:

```shell
dotnet tool update -g skills
```

A successful upgrade reports the version change:

```text
Tool 'skills' was successfully updated from version 0.1.0 to version 0.3.0.
```

If you no longer need the existing lock state and want to start fresh, delete the lock file and re-install your skills. The project lock lives at `skills-lock.json` in your working directory; the global lock lives at `~/.local/share/skills/.skill-lock.json` (or under `$XDG_DATA_HOME/skills` if that variable is set).

> [!WARNING]
> **Deleting a lock file discards update tracking.** The CLI uses the lock file to know what is installed and to check for updates. After deleting it, run `dnx skills add` again for each skill so the CLI can rebuild the lock. The installed skill folders themselves are not removed by deleting the lock.

# Bundled binary assets are missing from a skill

A skill installs, but a binary file it ships (an image, a model, a compiled tool) is a tiny placeholder or absent.

Cause: the CLI disables Git LFS while cloning, so only LFS pointer files are fetched, not the large binaries they track. This keeps clones fast and shallow, but it means LFS-tracked assets do not arrive with the skill.

To fix it, change how the skill stores its assets:

- Store skill assets without Git LFS so they travel as ordinary files in the repository.
- Vendor the assets directly into the skill folder (commit the real files alongside `SKILL.md`).

If you author the skill yourself, see [Authoring Skills](./authoring-skills.md) for how to lay out bundled resources.

# "Failed to remove" a skill

`dnx skills remove` reports that it could not remove a skill:

```text
$ dnx skills remove "My Skill" --yes
# exit 1

Failed to remove 1 skill(s)
  My Skill: ...
```

Cause: the CLI removes a skill by the sanitized, lowercase, hyphenated name it uses on disk (for example `my-skill`). A folder created outside `skills` whose name does not match that sanitized form is not something the CLI will delete automatically, so it reports the failure rather than falsely claiming success. The CLI also refuses to delete a real non-empty directory or file it did not create, which protects your own data.

To fix it, delete the folder yourself from the skills directory. For a project install that is `./.agents/skills/<folder>`; for the corresponding agent directory it is, for example, `./.claude/skills/<folder>`. For a global install the canonical store is `~/.agents/skills/<folder>`.

```shell
rm -rf "./.agents/skills/My Skill"
```

Run `dnx skills list` afterward to confirm the skill is gone. The listing no longer shows the removed skill:

```text
$ dnx skills list

No project skills found.
Try listing global skills with -g
```

# Next steps

- [Reference](./reference.md): every command, flag, exit code, and environment variable in one place.
- [Installing Skills](./installing-skills.md): sources, scopes, agent targeting, and the symlink versus copy model.
- Still stuck? Ask in the [ChilliCream Slack](https://slack.chillicream.com/).

---

### Website Content Docs Strawberryshake Configuration (`website/content/docs/strawberryshake/configuration.md`)

---
title: Configuration
description: "Configure the Strawberry Shake code generator through `.graphqlrc.json`: client name, namespace, schema URL, transport profiles, and generator settings."
---

Strawberry Shake is configured by altering the `.graphqlrc.json` at the root of your project.
All settings go into the `extensions.strawberryShake` object.
Here is a full configuration with all possibilities:

```json
{
  // The path of the schema file, that will be used to generate the client.
  // This setting may also be used by other tooling, because it is a default field of graphql-spec
  "schema": "schema.graphql",
  // The selector that determines, what files will be regarded as graphql documents
  // This setting may also be used by other tooling, because it is a default field of graphql-spec
  "documents": "**/*.graphql",
  "extensions": {
    // Here do only Strawberry Shake specific settings live
    "strawberryShake": {
      // The name of the generated client
      "name": "ChatClient",
      // The namespace of all the generated files of the client
      "namespace": "Demo",
      // The URL of the GraphQL api you want to consume with the client
      "url": "https://workshop.chillicream.com/graphql/",
      // The access level modifier of the generated client
      "accessModifier": "public",
      // Allows fields whose return types differ only in nullability to be merged in a selection set
      // Requires the EnableCovariantFieldMerging schema option on the Hot Chocolate server
      "enableCovariantFieldMerging": false
    }
  }
}
```

---

### Website Content Docs Strawberryshake Scalars (`website/content/docs/strawberryshake/scalars.md`)

---
title: "Scalars"
description: "GraphQL scalars Strawberry Shake supports out of the box, plus custom scalars via `@runtimeType` and `@serializationType` and your own `ScalarSerializer`."
---

Strawberry Shake supports the following scalars out of the box:

| Type          | Description                                                                                                         |
| ------------- | ------------------------------------------------------------------------------------------------------------------- |
| Any           | The [Any][1] scalar type represents any valid GraphQL value.                                                        |
| Base64String  | The [Base64String][2] scalar type represents an array of bytes encoded as a Base64 string.                          |
| Boolean       | The [Boolean][3] scalar type represents `true` or `false`.                                                          |
| Byte          | The [Byte][4] scalar type represents a signed 8-bit integer.                                                        |
| ByteArray     | Base64-encoded array of bytes. (DEPRECATED, use `Base64String`)                                                     |
| Date          | The [Date][5] scalar type represents a date in UTC.                                                                 |
| DateTime      | The [DateTime][6] scalar type represents a date and time with time zone offset information.                         |
| Decimal       | The [Decimal][7] scalar type represents a decimal floating-point number with high precision.                        |
| Duration      | The [Duration][8] scalar type represents a duration of time.                                                        |
| Float         | The [Float][9] scalar type represents signed double-precision finite values as specified by [IEEE 754][10].         |
| ID            | The [ID][11] scalar type represents a unique identifier, often used to refetch an object or as the key for a cache. |
| Int           | The [Int][12] scalar type represents a signed 32-bit numeric non-fractional value.                                  |
| LocalDate     | The [LocalDate][13] scalar type represents a date without time or time zone information.                            |
| LocalDateTime | The [LocalDateTime][14] scalar type represents a date and time without time zone information.                       |
| LocalTime     | The [LocalTime][15] scalar type represents a time of day without date or time zone information.                     |
| Long          | The [Long][16] scalar type represents a signed 64-bit integer.                                                      |
| Short         | The [Short][17] scalar type represents a signed 16-bit integer.                                                     |
| String        | The [String][18] scalar type represents textual data, represented as a sequence of Unicode code points.             |
| UnsignedByte  | The [UnsignedByte][19] scalar type represents an unsigned 8-bit integer.                                            |
| UnsignedInt   | The [UnsignedInt][20] scalar type represents an unsigned 32-bit integer.                                            |
| UnsignedLong  | The [UnsignedLong][21] scalar type represents an unsigned 64-bit integer.                                           |
| UnsignedShort | The [UnsignedShort][22] scalar type represents an unsigned 16-bit integer.                                          |
| URI           | The [URI][23] scalar type represents a Uniform Resource Identifier (URI) as defined by RFC 3986.                    |
| URL           | The [URL][24] scalar type represents a Uniform Resource Locator (URL) as defined by RFC 3986.                       |
| UUID          | The [UUID][25] scalar type represents a Universally Unique Identifier (UUID) as defined by RFC 9562.                |

[1]: https://scalars.graphql.org/chillicream/any.html
[2]: https://scalars.graphql.org/chillicream/base64-string.html
[3]: https://spec.graphql.org/September2025/#sec-Boolean
[4]: https://scalars.graphql.org/chillicream/byte.html
[5]: https://scalars.graphql.org/chillicream/date.html
[6]: https://scalars.graphql.org/chillicream/date-time.html
[7]: https://scalars.graphql.org/chillicream/decimal.html
[8]: https://scalars.graphql.org/chillicream/duration.html
[9]: https://spec.graphql.org/September2025/#sec-Float
[10]: https://en.wikipedia.org/wiki/IEEE_floating_point
[11]: https://spec.graphql.org/September2025/#sec-ID
[12]: https://spec.graphql.org/September2025/#sec-Int
[13]: https://scalars.graphql.org/chillicream/local-date.html
[14]: https://scalars.graphql.org/chillicream/local-date-time.html
[15]: https://scalars.graphql.org/chillicream/local-time.html
[16]: https://scalars.graphql.org/chillicream/long.html
[17]: https://scalars.graphql.org/chillicream/short.html
[18]: https://spec.graphql.org/September2025/#sec-String
[19]: https://scalars.graphql.org/chillicream/unsigned-byte.html
[20]: https://scalars.graphql.org/chillicream/unsigned-int.html
[21]: https://scalars.graphql.org/chillicream/unsigned-long.html
[22]: https://scalars.graphql.org/chillicream/unsigned-short.html
[23]: https://scalars.graphql.org/chillicream/uri.html
[24]: https://scalars.graphql.org/chillicream/url.html
[25]: https://scalars.graphql.org/chillicream/uuid.html

# Custom Scalars

As an addition to the scalars listed above, you can define your own scalars for the client.
A scalar has two representations: the `runtimeType` and the `serializationType`.
The `runtimeType` refers to the type you use in your dotnet application.
The `serializationType` is the type that is used to transport the value.

Let us explore this with the example of `DateTime`. The server serializes a date into a string on the server.
It is transported as a string over the wire:

```json
{
  "user": {
    // the serializationType in this case is string
    "registrationDate": "02-04-2001T12:00:03Z"
  }
}
```

The `registrationDate` in our .NET client, should on the other hand be represented as a `System.DateTime`.

```csharp
public partial class GetUser_User : IEquatable<GetUser_User>, IGetUser_User
{
    // ....

    // The runtimeType is DateTime
    public DateTime? RegistrationDate { get; }

    // ....
}
```

By default, all custom scalars are treated like the `String` scalar.
This means, that the client expects a string value and will deserialize it to a `System.String`.

If you want to change the `serializationType` or/and the `runtimeType` of a scalar, you have to specify the desired types in the `schema.extensions.graphql`.
You can declare a scalar extension and add the `@serializationType` or/and the `@runtimeType` directive.

```graphql
"""
Defines the serialization type of a scalar.
"""
directive @serializationType(
  """
  The fully qualified .NET type name.
  """
  name: String!

  """
  Indicates whether the specified type is a value type (struct).
  """
  valueType: Boolean = false
) on SCALAR

"""
Defines the runtime type of a scalar.
"""
directive @runtimeType(
  """
  The fully qualified .NET type name.
  """
  name: String!

  """
  Indicates whether the specified type is a value type (struct).
  """
  valueType: Boolean = false
) on SCALAR

"""
Represents an integer value that is greater or equal to 0.
"""
extend scalar PositiveInt
    @serializationType(name: "global::System.Int32")
    @runtimeType(name: "global::System.Int32")
```

As soon as you specify custom serialization and runtime types you also need to provide a serializer for the type.

## Serializer

A scalar identifies its serializer by the scalar name, runtime- and serialization type.
You have to provide an `ISerializer` as soon as you change the `serializationType` or the `runtimeType`.
Use the base class `ScalarSerializer<TValue>` or `ScalarSerializer<TSerializer, TRuntime>` to create your custom serializer.

### Simple Example

If the serialization and the value type are identical, you can just use the `ScalarSerializer` base class.

_schema.extensions.graphql_

```graphql
extend scalar PositiveInt
  @serializationType(name: "global::System.Int32")
  @runtimeType(name: "global::System.Int32")
```

_serializer_

```csharp
public class PositiveIntSerializer : ScalarSerializer<int>
{
    public PositiveIntSerializer()
        : base(
            // the name of the scalar
            "PositiveInt")
    {
    }
}
```

_configuration_

```csharp
serviceCollection.AddSerializer<PositiveIntSerializer>();
```

> [!WARNING]
> When using a value type (struct) with `@serializationType` or `@runtimeType`, you must set `valueType: true` to ensure correct code generation.<br />
> This is not required for intrinsic primitive value types already supported as built-in scalars by Strawberry Shake (e.g., `int`, `float`, `bool`).<br />
> Example: `#!sdl @serializationType(name: "global::System.Numerics.Vector2", valueType: true)`

### Any or JSON

Some GraphQL schemas contain untyped fields, whose types are often called `Any` or `JSON`. Strawberry Shake allows you to access these fields.

By default Strawberry Shake will use the built-in `AnySerializer` to represent these fields as `JsonElement`. If you want a different representation or use a different JSON library you can do so by providing a custom serializer that handles JSON scalars.

JSON objects are internally handled as `JsonElement` provided by `System.Text.Json`. You can use this to handle serialization by yourself.

> [!NOTE]
> If you want the raw json from the `JsonElement` use `GetRawText`.
> In order to have a custom serializer you need to specify runtime and serialization type.

_schema.extensions.graphql_

```graphql
extend scalar Any
  @serializationType(name: "global::System.Object")
  @runtimeType(name: "global::System.Text.Json.JsonElement")
```

Also you need to provide a custom serializer to handle the parsing of the `JsonElement` to whatever type you desire.

_serializer_

```csharp
public class MyJsonSerializer : ScalarSerializer<JsonElement, object>
{
    public MyJsonSerializer(string typeName = BuiltInScalarNames.Any)
        : base(typeName)
    {
    }

    public override object Parse(JsonElement serializedValue)
    {
        // handle the serialization of the JsonElement
    }

    protected override JsonElement Format(object runtimeValue)
    {
        // handle the serialization of the runtime representation in case
        // the scalar is used as a variable.
    }
}
```

### Advanced Example

Your schema contains `X509Certificate`'s. These are serialized to `Base64` on the server and transported as strings.

_schema.extensions.graphql_

```graphql
extend scalar X509Certificate
  @serializationType(name: "global::System.String")
  @runtimeType(
    name: "global::System.Security.Cryptography.X509Certificates.X509Certificate2"
  )
```

_serializer_

```csharp
public class X509CertificateSerializer
    : ScalarSerializer<string, X509Certificate2>
{
    public X509CertificateSerializer()
       : base(
           // the name of the scalar
           "X509Certificate")
    {
    }

    // Parses the value that is returned from the server (Output)
    public override X509Certificate2 Parse(string serializedValue)
    {
        return new X509Certificate2(Convert.FromBase64String(serializedValue));
    }

    // Formats the value to send to the server (Input)
    protected override string Format(X509Certificate2 runtimeValue)
    {
        return Convert.ToBase64String(runtimeValue.Export(X509ContentType.Cert));
    }
}
```

_configuration_

```csharp
serviceCollection.AddSerializer<X509CertificateSerializer>();
```

---

### Website Content Docs Strawberryshake Subscriptions (`website/content/docs/strawberryshake/subscriptions.md`)

---
title: "Subscriptions"
description: "Use GraphQL subscriptions in Strawberry Shake over WebSockets: configure `ConfigureWebSocketClient` and consume real-time results with `Watch()`."
---

> 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.

Subscriptions in GraphQL represent real-time events that are represented as a stream of query responses. In most cases, subscriptions are used over WebSockets but can also be used with other protocols.

> For transport questions, please head over to the [network docs](./networking/index.md).

GraphQL subscriptions can be used through reactive APIs like queries. Instead of a single network request, the store will subscribe to the GraphQL response stream and update the store for each new result.

# Setup

> This section will be based on the [getting started tutorial](./get-started/index.md).

To create a subscription, we start with everything in Strawberry Shake by creating a GraphQL file.

1. Create a new GraphQL file and call it `OnSessionUpdated` with the following content.

```graphql
subscription OnSessionUpdated {
  onSessionScheduled {
    title
  }
}
```

2. Add the [StrawberryShake.Transport.WebSockets](https://www.nuget.org/packages/StrawberryShake.Transport.WebSockets) package to your project.

3. Build your project so that the code-generator kicks in.

4. Configure the transport settings for the WebSocket.

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

5. A subscription can now be used like any other query by subscribing to it.

```csharp
var session =
    ConferenceClient
        .OnSessionUpdated
        .Watch()
        .Subscribe(result =>
        {
            // do something with the result
        });
```

Remember, `session` is an `IDisposable` and will stop receiving events when `Dispose` is invoked.

# Websocket Authentication

When working with GraphQL subscriptions over WebSockets, you may want to authenticate incoming WebSocket connections using JSON Web Tokens. Normally, HTTP headers are sent with each request for standard APIs, but WebSockets behave differently. After a successful HTTP handshake, the protocol is "upgraded" to WebSockets, and additional headers cannot be easily injected for subsequent messages.

Instead, the recommended approach is to send your token via the `connection_init` message when the WebSocket connection is first established. Hot Chocolate allows you to intercept this initial message, extract the token, and then authenticate the user in a way similar to standard HTTP requests.

An example implementation of this approach can be found in the [Hot Chocolate Examples repository](https://github.com/ChilliCream/hotchocolate-examples/tree/master/misc/WebsocketAuthentication).

## Why a Special Approach for WebSockets

- **Single HTTP Handshake**: A WebSocket connection is established once. After that, you cannot update HTTP headers on the same connection.
- **`connection_init` Payload**: GraphQL subscription clients send a `connection_init` message when establishing the subscription. This payload can include extra properties (e.g., `authorization`), which Hot Chocolate can use for authentication.
- **Long-Lived Connections**: Because WebSockets are persistent, tokens might remain valid for the entire duration of the connection. It is advisable to ensure that you handle token expiration appropriately-often by closing the connection if security policies require it.

## Core Concepts

1. **Stub (or "Skip") Authentication Scheme**
   The initial WebSocket upgrade request is directed to a "stub" authentication scheme that simply indicates "no authentication result" for upgrade requests. This prevents the request from failing before you can intercept and handle the token manually.

2. **Forwarding the Default Scheme**
   In standard HTTP scenarios, the default scheme (e.g., JWT bearer) is used to authenticate. However, if the request is recognized as a WebSocket upgrade, the framework forwards it to the "stub" scheme first. That way, you don’t attempt to validate a token at the moment of the upgrade handshake.

3. **Intercepting `connection_init`**
   Once the WebSocket is established, the client sends `connection_init` containing authentication data. A custom `SocketSessionInterceptor` (or similar) reads the token from `connection_init` (e.g., under a key like `authorization`), stores it in the `HttpContext`, and triggers a fresh authentication attempt-this time using the real JWT bearer scheme.

4. **Hot Chocolate Integration**
   Hot Chocolate's subscription middleware allows you to plug into the subscription lifecycle. By customizing the session interceptor (`OnConnectAsync`), you can decide whether to accept or reject the connection based on successful authentication.

## Testing the Flow

1. **Open Nitro**
   Use a local instance of Nitro (e.g., `https://localhost:5095/graphql`) to send GraphQL queries and subscriptions.

2. **Retrieve an Access Token**
   Request a token from your `/token` endpoint. This endpoint should return a valid JWT that is trusted by your API.

3. **Configure Nitro**
   - In Nitro, open the **Settings** of your document / api.
   - Under **Authentication**, choose **Bearer Token** and paste your JWT.
   - Nitro will automatically include the token in the `connection_init` message under an `authorization` parameter when opening a WebSocket connection.

4. **Run Your Subscription**
   Execute the subscription query of your choice. For example:

   ```graphql
   subscription {
     onTimedEvent {
       count
       isAuthenticated
     }
   }
   ```

   The server-side resolver can check `isAuthenticated` to demonstrate whether the current user is authenticated (based on the token you provided).

---

### Website Content Docs Strawberryshake Tooling (`website/content/docs/strawberryshake/tooling.md`)

---
title: "Tooling"
description: "Strawberry Shake CLI tooling: `dotnet graphql init`, `update`, and `download` commands to scaffold a client project and keep its GraphQL schema in sync."
---

> 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 comes with some tools that integrate into the dotnet CLI and help to setup a project or update the schema of a project.

# Initialize Project

`#!shell dotnet graphql init http://localhost/graphql`

The `init` command allows you to initialize a C# project for use with Strawberry Shake. It essentially creates the initial configuration file `.graphqlrc.json` and downloads the GraphQL schema.

`dotnet graphql init {url} [-p|--Path] [-n|--clientName] [--token] [--scheme] [--tokenEndpoint] [--clientId] [--clientSecret] [--scope] [-x|--headers]`

| Argument           | Description                                                                                          |
| ------------------ | ---------------------------------------------------------------------------------------------------- |
| -p or --path       | The path where the project is located.                                                               |
| -n or --clientName | The name of the client, which will also become the name of the client class in C#.                   |
| --token            | A token to interact with the server.                                                                 |
| --scheme           | The token schema that shall be used.                                                                 |
| --tokenEndpoint    | The token endpoint that shall be used to acquire a new token.                                        |
| --clientId         | The client ID that shall be used when interacting with the `tokenEndpoint`.                          |
| --clientSecret     | The client secret that shall be used when interacting with the `tokenEndpoint`.                      |
| --scope            | The scope (can be used multiple times) that shall be used when interacting with the `tokenEndpoint`. |
| -x or --headers    | The headers adds additional custom headers. Example: --headers key1=value1 --headers key2=value2     |

# Update Project

`dotnet graphql update`

The update command allows you to update the local GraphQL schema with the newest version of the GraphQL server.

`dotnet graphql update [-p|--Path] [-u|--uri] [--token] [--scheme] [--tokenEndpoint] [--clientId] [--clientSecret] [--scope] [-x|--headers]`

| Argument        | Description                                                                                          |
| --------------- | ---------------------------------------------------------------------------------------------------- |
| -p or --path    | The path where the project is located.                                                               |
| -u or --uri     | The GraphQL server URI that shall be used instead of the one in the `graphqlrc.json`                 |
| --token         | A token to interact with the server.                                                                 |
| --scheme        | The token schema that shall be used.                                                                 |
| --tokenEndpoint | The token endpoint that shall be used to acquire a new token.                                        |
| --clientId      | The client ID that shall be used when interacting with the `tokenEndpoint`.                          |
| --clientSecret  | The client secret that shall be used when interacting with the `tokenEndpoint`.                      |
| --scope         | The scope (can be used multiple times) that shall be used when interacting with the `tokenEndpoint`. |
| -x or --headers | The headers adds additional custom headers. Example: --headers key1=value1 --headers key2=value2     |

# Download Schema

`#!shell dotnet graphql download http://localhost/graphql`

The download command allows downloading a GraphQL schema from any GraphQL server.

`dotnet graphql download {url} [-f|--fileName] [--token] [--scheme] [--tokenEndpoint] [--clientId] [--clientSecret] [--scope] [-x|--headers]`

| Argument         | Description                                                                                          |
| ---------------- | ---------------------------------------------------------------------------------------------------- |
| -f or --fileName | The name of the file name. If not specified, the file will be called `Schema.graphql`.               |
| --token          | A token to interact with the server.                                                                 |
| --scheme         | The token schema that shall be used.                                                                 |
| --tokenEndpoint  | The token endpoint that shall be used to acquire a new token.                                        |
| --clientId       | The client ID that shall be used when interacting with the `tokenEndpoint`.                          |
| --clientSecret   | The client secret that shall be used when interacting with the `tokenEndpoint`.                      |
| --scope          | The scope (can be used multiple times) that shall be used when interacting with the `tokenEndpoint`. |
| -x or --headers  | The headers adds additional custom headers. Example: --headers key1=value1 --headers key2=value2     |

---

### Website Content Docs Fusion Adapters Mcp (`website/content/docs/fusion/adapters/mcp.md`)

---
title: "MCP Adapter"
description: "Expose a Fusion gateway as an MCP server with `AddMcp()` and `MapGraphQLMcp()`, serving Nitro or custom tools and prompts to AI assistants over Streamable HTTP."
---

The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard that lets AI assistants connect to external systems through a uniform tool and prompt interface. The `HotChocolate.Fusion.Adapters.Mcp` package turns a Fusion gateway into an MCP server. You supply tool and prompt definitions through an `IMcpStorage`, and the adapter handles execution, transport, and live updates. Definitions can come from Nitro (file-based authoring with a publish workflow) or from a custom `IMcpStorage` you build (programmatic, database-backed, or any source you choose).

You wire MCP onto an existing Fusion gateway with two calls: `AddMcp()` during service registration and `MapGraphQLMcp()` during endpoint mapping. The adapter exposes the MCP server over Streamable HTTP at `/graphql/mcp` by default, so any MCP client (Claude Desktop, an editor extension, an agent runtime) can connect directly to the gateway.

This page covers wiring and configuration. For authoring tools and prompts, see the [Nitro MCP](../../nitro/adapters/mcp.md) section.

# Prerequisites

You need an existing Fusion gateway. If you do not have one yet, follow the [Getting Started](../getting-started.md) tutorial first.

Add the adapter package to the gateway project:

```shell
dotnet add package HotChocolate.Fusion.Adapters.Mcp
```

# Enabling MCP on the Gateway

Two adapter calls turn a Fusion gateway into an MCP server. `AddMcp()` registers the MCP server, schema services, and a startup warmup that loads tool and prompt definitions from storage. `MapGraphQLMcp()` exposes the MCP transport endpoints.

```csharp
builder
    .AddGraphQLGateway()
    .AddMcp();
    // Storage is required: see "Connecting a Tool and Prompt Source" below.

// ...

app.MapGraphQLMcp();
```

The gateway needs a tool and prompt source. Without one, `MapGraphQLMcp()` throws `InvalidOperationException` during startup. Wire up storage by either [using Nitro](#using-nitro-for-tools-and-prompts) or providing a custom `IMcpStorage`.

# Connecting a Tool and Prompt Source

The adapter does not ship tools or prompts of its own. It asks an `IMcpStorage` implementation for them at startup, and listens for change notifications afterwards. You have two options:

1. **Use Nitro** (recommended for production). Nitro publishes versioned MCP feature collections to the gateway and supplies an `IMcpStorage` automatically. Skip ahead to [Using Nitro](#using-nitro-for-tools-and-prompts).
2. **Provide your own `IMcpStorage`** for self-hosted scenarios where you manage tool definitions outside Nitro.

To register a custom storage, implement `IMcpStorage` and pass it to `AddMcpStorage()`:

```csharp
builder
    .AddGraphQLGateway()
    .AddMcp()
    .AddMcpStorage<MyMcpStorage>();
```

Three overloads cover the common registration patterns:

| Overload                                             | Use when                                                                  |
| ---------------------------------------------------- | ------------------------------------------------------------------------- |
| `AddMcpStorage(IMcpStorage instance)`                | You already have a singleton instance.                                    |
| `AddMcpStorage<T>()`                                 | You want DI to construct the storage. `T` is activated from app services. |
| `AddMcpStorage(Func<IServiceProvider, IMcpStorage>)` | You need a factory for custom construction or scoping.                    |

`IMcpStorage` returns `OperationToolDefinition` and `PromptDefinition` collections, and exposes `IObservable` streams so the gateway can apply update and remove events without restarting. Reach for this extension point only when you cannot use Nitro, since implementing it correctly involves change diffing, caching, and reactive subscriptions.

# Configuring the MCP Server

`AddMcp()` accepts two configuration delegates. The first targets `McpServerOptions` (server behavior), the second targets `IMcpServerBuilder` (tool, prompt, and resource registration from the underlying MCP SDK):

```csharp
builder
    .AddGraphQLGateway()
    .AddMcp(
        configureServerOptions: options =>
        {
            options.InitializationTimeout = TimeSpan.FromSeconds(30);
        },
        configureServer: server =>
        {
            // Register additional MCP server features here.
        });
```

Tools registered through `configureServer` appear alongside the GraphQL-derived tools, so you can mix native MCP tools with operation tools in the same gateway.

# Mapping the MCP Endpoint

`MapGraphQLMcp()` accepts two optional arguments:

```csharp
app.MapGraphQLMcp(pattern: "/graphql/mcp", schemaName: null);
```

- **`pattern`**: the URL prefix for the MCP transport. Defaults to `/graphql/mcp`. Change it when the default conflicts with another route or when you expose multiple gateways from the same host.
- **`schemaName`**: the named Fusion schema to expose. The adapter resolves this automatically when the gateway has a single schema. Pass it explicitly when the gateway hosts multiple schemas, so each schema gets its own MCP endpoint:

```csharp
app.MapGraphQLMcp("/graphql/public/mcp", schemaName: "Public");
app.MapGraphQLMcp("/graphql/internal/mcp", schemaName: "Internal");
```

The endpoint speaks Streamable HTTP. POST carries JSON-RPC requests and returns either `text/event-stream` for streaming responses or `202 Accepted` for queued ones.

# Using Nitro for Tools and Prompts

Nitro is the easiest way to manage MCP tools and prompts. You author tools and prompts on disk, upload them as a tagged version of a feature collection with the Nitro CLI, and publish that version to a stage. The gateway loads the collection from the configured stage and picks up new versions automatically. When Nitro is wired up alongside `AddMcp()`, it registers an `IMcpStorage` for you, so you do not call `AddMcpStorage()` yourself.

## Install the Nitro packages

```shell
dotnet add package ChilliCream.Nitro
dotnet add package ChilliCream.Nitro.Fusion
```

`ChilliCream.Nitro` is the core package and includes a source generator that emits an `AddDefaults()` extension method based on which integration packages are referenced in the project. With `ChilliCream.Nitro.Fusion` referenced, `AddDefaults()` calls `AddFusion()` for you.

## Wire Nitro into the gateway

Call `AddNitro().AddDefaults()` (or the explicit `AddNitro().AddFusion()`) before configuring the gateway. `AddNitro()` configures the shared connection options (`ApiId`, `ApiKey`, `Stage`) on `NitroServiceOptions`. `ModifyNitroOptions()` on the gateway builder configures gateway-specific options (MCP, OpenAPI, persisted operations, metrics, and so on):

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddNitro(o =>
    {
        o.ApiId = builder.Configuration["Nitro:ApiId"]!;
        o.ApiKey = builder.Configuration["Nitro:ApiKey"]!;
        o.Stage = builder.Configuration["Nitro:Stage"]!;
    })
    .AddDefaults();

builder
    .AddGraphQLGateway()
    .ModifyNitroOptions(o =>
    {
        // Modify Nitro options here.
    })
    .AddMcp();

var app = builder.Build();

app.MapGraphQL();
app.MapGraphQLMcp();

app.Run();
```

If you prefer environment variables over inline configuration, set `NITRO_API_ID`, `NITRO_STAGE`, and `NITRO_API_KEY`. The Nitro service options bind to these automatically and you can drop the `AddNitro` configuration delegate entirely.

> Order matters: `AddNitro().AddDefaults()` (or `AddFusion()`) must run before the gateway builder calls so that the Fusion gateway pipeline picks up the Nitro contributions during registration.

## What you get

With Nitro and MCP both enabled:

- The gateway loads the published MCP feature collection for the configured stage on startup.
- Tool and prompt definitions are cached locally so cold starts work without a round trip to Nitro.
- Stage change events flow over the Nitro change feed. When you publish a new version, the gateway updates its tool and prompt set in place, no restart required.
- If Nitro configuration is incomplete (any of `ApiId`, `ApiKey`, or `Stage` not set), MCP integration is disabled with a warning, the storage returns no definitions, and the gateway continues to start.
- If configuration is set but the API key is rejected, the storage uses the local cache when one is available and logs the sync failure. Without a usable cache, the exception propagates and host startup fails.

For authoring tools and prompts, publishing feature collection versions, and managing stages, see the [Nitro MCP](../../nitro/adapters/mcp.md) section.

# Troubleshooting

## `InvalidOperationException: Call AddMcp() when configuring the GraphQL server.`

`MapGraphQLMcp()` was called but `AddMcp()` was not registered on the gateway builder. Add `.AddMcp()` to the chain that starts with `AddGraphQLGateway()`.

## MCP endpoint returns 404 Not Found

The route pattern does not match what your client uses. The default is `/graphql/mcp`. If you passed a custom pattern to `MapGraphQLMcp()`, point your client at it.

## `InvalidOperationException: No IMcpStorage is registered for schema '<name>'.`

Two possible causes:

- **No storage source for that schema.** `AddMcp()` was registered, but no `IMcpStorage` is wired up. Either reference `ChilliCream.Nitro.Fusion` and call `AddNitro().AddDefaults()` (or `AddNitro().AddFusion()`), or register a custom storage with `AddMcpStorage(...)`.
- **Endpoint mapped to the wrong schema.** `MapGraphQLMcp(pattern, schemaName)` was called with a `schemaName` that does not match any gateway registered through `AddGraphQLGateway(name)` plus `AddMcp()`. With multiple named gateways, pass the matching name. With a single unnamed gateway, omit `schemaName` and the adapter resolves it automatically.

## Tools and prompts list is empty

Storage is registered but returned no definitions. With Nitro, ensure a published MCP feature collection version exists for the configured stage and that the API key has read access to it. With a custom `IMcpStorage`, verify that the implementation completes its initial fetch before the warmup task times out and that it returns the expected definitions.

## Nitro logs `MCP integration is disabled because Nitro is not properly configured.`

`NitroServiceOptions` is missing one or more of `ApiId`, `ApiKey`, or `Stage`. Set them through the `AddNitro()` configuration delegate or via the `NITRO_API_ID`, `NITRO_API_KEY`, and `NITRO_STAGE` environment variables.

# Next Steps

- Author tools and prompts and publish them to a stage in the [Nitro MCP](../../nitro/adapters/mcp.md) section.
- Deploy your gateway across stages with [Deployment and CI/CD](../deployment-and-ci-cd.md).

---

### Website Content Docs Fusion Connectors Apollofederation (`website/content/docs/fusion/connectors/apollofederation.md`)

---
title: "Apollo Federation Connector"
metaTitle: "Use Apollo Federation Subgraphs with Hot Chocolate Fusion"
description: "Put a Fusion gateway in front of your existing Apollo Federation subgraphs. Composition auto-detects Apollo Federation SDL and translates @key, @requires, and _entities into the GraphQL Federation model, with no changes to your subgraphs."
---

Fusion supports two subgraph protocols: GraphQL Federation and Apollo Federation. GraphQL Federation is the open specification (formerly the GraphQL Composite Schemas specification) that a Fusion gateway speaks directly. Apollo Federation is Apollo's model for distributed GraphQL. This page explains the Apollo Federation connector, which lets a Fusion gateway run Apollo Federation subgraphs.

The connector lets you put a Fusion gateway in front of existing Apollo Federation subgraphs without changing them. During composition, Fusion reads each subgraph's Apollo Federation SDL, detects Apollo Federation schemas, and translates them into the GraphQL Federation model. At runtime, the gateway speaks Apollo's wire protocol (the `_entities` field with typed representations) to Apollo Federation subgraphs. It speaks the GraphQL Federation protocol through lookup fields to GraphQL Federation subgraphs in the same graph. Your subgraphs keep their existing SDL, `__resolveReference` and reference resolvers, and deployment model.

Apollo Federation v2 has no separate mode to enable. Detection happens per source schema, so one graph can mix Apollo Federation subgraphs (in any language: Apollo Server, HotChocolate.ApolloFederation, graphql-java, and others) with GraphQL Federation subgraphs and compose them into the composed schema. Apollo Federation v1 requires an explicit source setting, as described in [Choose the Schema Acquisition Protocol](#choose-the-schema-acquisition-protocol).

Use this page when you want to run Apollo Federation subgraphs as they are. To move a subgraph to the GraphQL Federation protocol instead, see [Coming from Apollo Federation](../migration/coming-from-apollo-federation.md). The two protocols interoperate, so you can move one subgraph at a time.

# How the Connector Works

The connector does two things: it translates Apollo Federation schemas at composition time, and it uses Apollo's entity protocol at runtime.

```mermaid
flowchart LR
    Client --> Gateway["Fusion Gateway"]
    Gateway -->|"_entities protocol"| Apollo["Apollo Federation subgraph"]
    Gateway -->|"lookup fields"| GraphQLFed["GraphQL Federation subgraph"]
```

**At composition time**, Fusion inspects each source schema. When a v2 schema carries the Apollo Federation `@link` to `https://specs.apollo.dev/federation`, the composer recognizes it as an Apollo Federation subgraph and runs a translation pass over it. For a v1 schema, the source settings must opt into the legacy parser with the exact `"1.0"` marker shown below.

That pass maps Apollo Federation directives onto the GraphQL Federation model: `@key` becomes lookup fields, `@requires` becomes `@require`, `@external` fields are resolved into the model, and the Relay `node` field becomes a lookup. Fusion removes the Apollo Federation infrastructure types (`_service`, `_entities`, `_Entity`, `_Any`). The result is the composed schema. Clients do not see Apollo Federation directives or infrastructure types.

**At runtime**, when the gateway needs to resolve entity fields from an Apollo Federation subgraph, it calls that subgraph as an Apollo router would. It sends the `#!graphql _entities(representations: [...])` query with typed representations (`{ __typename, <key fields> }`) and reads the entities back. The gateway enters GraphQL Federation subgraphs through their lookup fields instead. Both paths run inside the same query plan.

Because Federation v2 detection happens per source schema, mixed graphs with v2 Apollo Federation sources need no graph-wide connector configuration. Federation v1 sources still require the exact per-source `"1.0"` marker. An Apollo Federation subgraph and a GraphQL Federation subgraph that both contribute fields to `Product` merge into one `Product` type in the composed schema.

# Getting the Subgraph Schema

Composition consumes Apollo Federation SDL in the form that the subgraph exposes through Apollo's `_service` field. For v2, this is the `@link`-carrying document that still contains `@key`, `@requires`, `@external`, and the other Federation directives.

You can let Nitro download live schemas during composition, or save each schema to a file first.

## Download Live Schemas with Nitro

For each remote source, repeat both `--source-schema-url` and `--source-schema-settings-file`. Nitro pairs the first URL with the first settings file, the second URL with the second settings file, and so on. Keep each pair adjacent so the relationship is visible in scripts.

Local schema files remain independent. They continue to use the companion settings file next to the schema, not `--source-schema-settings-file`.

This example composes two live Apollo subgraphs and one saved GraphQL Federation schema:

```shell
nitro fusion compose \
  --source-schema-url https://products.example.com/graphql \
  --source-schema-settings-file ./products/schema-settings.json \
  --source-schema-url https://reviews.example.com/graphql \
  --source-schema-settings-file ./reviews/schema-settings.json \
  --source-schema-file ./inventory/schema.graphqls \
  --archive gateway.far
```

After a successful composition, Nitro prints:

```text
✅ Composite schema written to '/absolute/path/to/gateway.far'.
```

The URL option controls where Nitro acquires the schema during composition. The paired settings file still controls how the gateway reaches the source at runtime. These URLs can differ.

For an Apollo Federation v2 endpoint, use a settings file such as:

```json
{
  "name": "Products",
  "transports": {
    "http": {
      "url": "https://products.internal/graphql"
    }
  },
  "extensions": {
    "chillicream": {
      "apolloFederationSupport": {
        "version": "2.0"
      }
    }
  }
}
```

## Choose the Schema Acquisition Protocol

The `apolloFederationSupport` marker selects how Nitro uses the paired URL:

| Marker        | HTTP request                                      | Composition behavior                                                           |
| ------------- | ------------------------------------------------- | ------------------------------------------------------------------------------ |
| Absent        | GET the exact supplied URL                        | Treat the response body as plain SDL. A returned v2 `@link` is still detected. |
| Exact `"1.0"` | POST an Apollo `#!graphql _service { sdl }` query | Enable Federation v1 composition for the returned SDL.                         |
| Exact `"2.0"` | POST an Apollo `#!graphql _service { sdl }` query | Use normal v2 `@link` detection for the returned SDL.                          |

The support object must contain only the `version` property. Only exact `"1.0"` and `"2.0"` values are accepted. Other values, whitespace variants, and extra properties fail validation before Nitro sends a schema request.

For a Federation v1 source, use the same settings shape with the v1 marker:

```json
{
  "name": "Products",
  "transports": {
    "http": {
      "url": "https://products.internal/graphql"
    }
  },
  "extensions": {
    "chillicream": {
      "apolloFederationSupport": {
        "version": "1.0"
      }
    }
  }
}
```

Raw Federation v1 SDL without the `"1.0"` marker remains unsupported. The marker is preserved in the archived source settings so future composition keeps the same acquisition contract, but it is removed from runtime gateway settings.

## Save the SDL to a File

To export a schema yourself, query the subgraph:

```graphql
query {
  _service {
    sdl
  }
}
```

Save the `#!graphql _service { sdl }` value unchanged to a `.graphqls` file. A schema stripped of its Federation directives cannot be detected and translated. Give each file a companion settings file based on the schema file name. For example, `products.graphqls` uses `products-settings.json`. See the [Schema Settings File Reference](../cli.md#schema-settings-file-reference) for the settings file format.

# Composing an Apollo Federation Subgraph



Compose Apollo Federation subgraphs the same way you compose GraphQL Federation subgraphs. Federation v2 has no compose flag to set. Point `nitro fusion compose` at the source schema files, and the composer detects and translates v2 subgraphs automatically. Federation v1 sources still need the exact per-source `"1.0"` marker:

```shell
nitro fusion compose \
  --source-schema-file ./accounts/schema.graphqls \
  --source-schema-file ./reviews/schema.graphqls \
  --archive gateway.far
```

After a successful composition, Nitro prints:

```text
✅ Composite schema written to '/absolute/path/to/gateway.far'.
```

You can list Apollo Federation and GraphQL Federation source schema files in the same command. Composition produces a single `.far` archive that contains the composed schema. If a subgraph uses an Apollo Federation feature that Fusion does not yet support, composition fails with a specific error code (see [Current Limitations](#current-limitations)).

For the full command reference, see [nitro fusion compose](../cli.md#nitro-fusion-compose). For the composition rules and the complete log-code list, see [Composition](../composition.md).

# What Translates to What

Composition resolves Apollo Federation constructs before clients see the schema. The composed schema follows the GraphQL Federation model, so clients and downstream tools do not see Apollo Federation directives.

| Apollo Federation                          | Handled during composition as                                                                     |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| `@key(fields: "...")`                      | Generated lookup fields, one per key. Single, composite, and nested keys are supported.           |
| `_entities` / `__resolveReference`         | Kept as the runtime contract. The gateway calls them over the `_entities` protocol like a router. |
| `@requires(fields: "...")`                 | Translated to `@require`, including nested object requirements and requirement chains.            |
| `@provides(fields: "...")`                 | Honored. The `@external` fields it references are kept resolvable from the providing subgraph.    |
| `@external`                                | External fields are resolved into the model.                                                      |
| `@shareable`                               | Preserved. Key fields are marked shareable automatically.                                         |
| `@override(from: "...")`                   | Preserved as override semantics.                                                                  |
| `@inaccessible`                            | Preserved.                                                                                        |
| `@tag`                                     | Preserved.                                                                                        |
| `@interfaceObject`                         | Projects fields onto the same-named interface and compatible implementations.                     |
| Union members contributed across subgraphs | Merged into a single union in the composed schema.                                                |

# Shareable Abstract Field Routing

Apollo Federation subgraphs can define the same shareable field while declaring different members for its interface or union result type. Fusion offers two policies for routing selections under inline fragments and other type conditions:

| C# setting value     | CLI value              | Execution-schema value | Behavior                                                                                                                                                                  |
| -------------------- | ---------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SourceLocal`        | `source-local`         | `SOURCE_LOCAL`         | Routes type-conditioned selections according to the source schema that resolves the shareable field. This is the default, including for archives that predate the policy. |
| `CommonRuntimeTypes` | `common-runtime-types` | `COMMON_RUNTIME_TYPES` | Routes only runtime types shared by viable Apollo providers: those in current scope, directly reachable through one entity lookup, or non-external at an operation root.  |

`CommonRuntimeTypes` does not filter objects returned by a source or change `__typename`. A source-specific object can still appear in the response, while fields requested only through a type condition that was not routed can be `null`.

Use `CommonRuntimeTypes` to reproduce the conservative routing exercised by the legacy `PartialUnion` and `PartialUnionComplex` federation gateway-audit cases, or whenever viable providers expose different members of the same interface or union. Select it while composing:

```shell
nitro fusion compose \
  --source-schema-file ./products/schema.graphqls \
  --source-schema-file ./reviews/schema.graphqls \
  --shareable-field-runtime-type-routing common-runtime-types \
  --archive gateway.far
```

After a successful composition, Nitro prints the archive path:

```text
✅ Composite schema written to '/absolute/path/to/gateway.far'.
```

To change the policy in an existing archive, run:

```shell
nitro fusion settings set shareable-field-runtime-type-routing common-runtime-types \
  --archive gateway.far
```

Nitro reports `Composed new configuration.` after it recomposes the archive. With Aspire, set `GraphQLCompositionSettings.ShareableFieldRuntimeTypeRouting` to `ShareableFieldRuntimeTypeRouting.CommonRuntimeTypes`. See [Composition Settings](../local-development.md#composition-settings).

Composition records the policy in the execution schema. For example, `common-runtime-types` produces:

```graphql
schema
  @fusion__execution(
    nodeResolution: GATEWAY
    shareableFieldRuntimeTypeRouting: COMMON_RUNTIME_TYPES
  ) {
  query: Query
}
```

The default is recorded as `shareableFieldRuntimeTypeRouting: SOURCE_LOCAL`.

# Interface Objects

Apollo Federation v2 interface objects compose with native GraphQL Federation interfaces. Keep the Apollo declaration and its Federation v2 import in the subgraph SDL:

```graphql
extend schema
  @link(
    url: "https://specs.apollo.dev/federation/v2.6"
    import: ["@key", "@interfaceObject"]
  )

type Media @key(fields: "id") @interfaceObject {
  id: ID!
  views: Int!
}
```

Fusion projects this stand-in's fields onto the interface and compatible implementations. Values produced by the stand-in remain opaque until Fusion recovers their concrete type through a source that knows the implementations.

Apollo Federation does not define Fusion's native `@implement` directive. When an Apollo implementing type redeclares a projected field, mark the compatible declarations with Apollo `@shareable`. Fusion uses that declaration as the explicit replacement contract. A redeclaration that is not shareable fails composition.

At runtime, Fusion enters the Apollo stand-in through `_entities`. Native GraphQL Federation interface objects use `@lookup` fields instead. Both source kinds can participate in the same interface.

See [Interface Objects](../interface-objects.md) for field projection, opaque identity, covering lookups, and native source-schema examples.

## Allow Non-Resolvable Interface Objects

By default, composition rejects an Apollo interface object whose key uses `resolvable: false` when Fusion cannot build the required route. This strict default catches projected fields that the gateway could not fetch.

For compatibility with an existing Apollo graph, opt in while composing the archive:

```shell
nitro fusion compose \
  --source-schema-file ./products/schema.graphqls \
  --source-schema-file ./reviews/schema.graphqls \
  --allow-non-resolvable-interface-objects \
  --archive gateway.far
```

After a successful composition, Nitro prints:

```text
✅ Composite schema written to '/absolute/path/to/gateway.far'.
```

To enable the option in an existing archive, run:

```shell
nitro fusion settings set allow-non-resolvable-interface-objects true \
  --archive gateway.far
```

Nitro reports `Composed new configuration.` after it recomposes the archive.

With Aspire composition, set the equivalent property:

```csharp
builder
    .AddProject<Projects.Gateway>("gateway-api")
    .WithNitroComposition(
        settings: new GraphQLCompositionSettings
        {
            AllowNonResolvableInterfaceObjects = true
        });
```

> This option applies only to source schemas detected as Apollo Federation schemas. It does not relax native interface-object satisfiability. Enabling it does not create a lookup route, so an unresolved selection produces a field error at runtime.

# Global Object Identification

If your Apollo Federation subgraphs implement the Relay `node` field (a `#!sdl Query.node(id: ID!): Node` field over a `Node` interface), the connector turns it into a lookup during composition. Keep the Apollo Federation SDL as exported. You do not need to add Fusion's `@lookup` directive to the field.

## Configure node resolution

Choose how the gateway resolves `node(id:)` when you compose the gateway archive. Fusion records the mode in the execution schema, so every compatible gateway that loads the archive uses the same behavior.

| CLI value       | Execution-schema value | Behavior                                                                                                                                                                                                                                                                                              |
| --------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gateway`       | `GATEWAY`              | The gateway decodes the ID, determines the object type, and routes the lookup to the source schema that owns that type. This is the default.                                                                                                                                                          |
| `source-schema` | `SOURCE_SCHEMA`        | The gateway forwards the opaque ID to a source schema with a public root `#!sdl Query.node(id: ID!): Node` lookup. That source schema determines the object type and can resolve the concrete `Node` implementations that it declares. Use this mode when the gateway cannot decode your identifiers. |

To use source-schema resolution, enable Global Object Identification and select the mode in the compose command:

```shell
nitro fusion compose \
  --source-schema-file ./accounts/schema.graphqls \
  --source-schema-file ./reviews/schema.graphqls \
  --archive gateway.far \
  --enable-global-object-identification \
  --node-resolution source-schema
```

To update an existing archive, enable Global Object Identification before changing the node-resolution setting:

```shell
nitro fusion settings set global-object-identification true \
  --archive gateway.far

nitro fusion settings set node-resolution source-schema \
  --archive gateway.far
```

For the settings command reference, see [nitro fusion settings set](../cli.md#nitro-fusion-settings-set). If you compose through Aspire, set `EnableGlobalObjectIdentification` to `true` and `NodeResolution` to `NodeResolution.SourceSchema` in `GraphQLCompositionSettings`. See [Composition settings](../local-development.md#composition-settings).

## Verify node resolution

After a successful composition, Nitro prints the archive path:

```text
✅ Composite schema written to '/absolute/path/to/gateway.far'.
```

The generated execution schema should also contain:

```graphql
schema @fusion__execution(nodeResolution: SOURCE_SCHEMA) {
  query: Query
}
```

Deploy a `SOURCE_SCHEMA` archive only to compatible gateways that support the `@fusion__execution` metadata. An older gateway can fall back to gateway-side ID decoding.

## Troubleshoot source-schema resolution

If composition reports `Source-schema node resolution requires global object identification to be enabled.`, you selected `source-schema` without enabling Global Object Identification. Add both `--enable-global-object-identification` and `--node-resolution source-schema` to the compose command.

Source-schema resolution covers only concrete `Node` implementations declared by a source schema with a public root `#!sdl Query.node(id: ID!): Node` lookup. If a valid dispatcher covers some implementations but another composite `Node` type is uncovered, composition emits an `UNSATISFIABLE_QUERY_PATH` warning. A request for an uncovered type can return `node: null`, with an error if the source resolver reports one. Add the type as a `Node` implementation in a source schema with a public root node lookup. If no public root dispatcher covers any `Node` implementation, composition fails.

See [GraphQL Global Object Identification](../entities-and-lookups.md#graphql-global-object-identification) for the required schema shape and the [composition log-code reference](../composition.md#log-codes-reference) for diagnostic details.

# Runtime Behavior

The gateway uses Apollo's entity protocol when it routes to Apollo Federation subgraphs.

**Entity batching.** When the gateway needs several entities of the same type from one subgraph, it sends one `_entities` call with all representations in the `representations` array. Identical representations are de-duplicated.

**Requirement threading.** When a field on one subgraph depends on data owned by another (the `@requires` case), the gateway resolves the required fields first. It then threads them into the representation it sends to the subgraph that needs them.

**Error propagation.** Errors returned by a subgraph and transport failures that prevent the gateway from reaching it are attached to the affected result paths and surfaced in the gateway response.

## Batching

Because entity keys travel in the `representations` argument, a single `_entities` call already resolves many entities at once. What is left to batch is the case where one plan wave needs several _different_ operations from the same subgraph, for example two `_entities` calls with different sub-selections.

For an Apollo Federation subgraph the gateway defaults to **alias batching only**: it merges those operations into one plain GraphQL operation with alias-prefixed root fields, which any spec-compliant GraphQL server can answer. Neither protocol extension, variable batching nor request batching, is assumed on this connector. Subgraphs connected through the default GraphQL connector keep the protocol-extension defaults instead.

If your federation server does accept JSON-array request batching, declare it in that source schema's settings and the gateway prefers it over alias batching:

```json
{
  "name": "Products",
  "transports": {
    "http": {
      "url": "https://products.internal/graphql",
      "capabilities": {
        "batching": {
          "requestBatching": true
        }
      }
    }
  }
}
```

Each flag you declare wins over the default on its own; the flags you leave out keep it. The snippet above therefore ends up with request batching **and** alias batching. To turn batching off completely for a federation subgraph, declare `"aliasBatching": false` as well.

See [Batching](../batching.md) for the three capabilities, how the gateway picks between them, and how errors are attributed to items of a merged operation.

# Current Limitations

The connector is under active development and ships as a preview. Composition rejects unsupported Apollo Federation features with a specific error code, so the gateway does not produce a schema that would misbehave at runtime.

- **Apollo Federation v1 requires explicit source settings.** Set `extensions.chillicream.apolloFederationSupport.version` to exact `"1.0"` for that source. Raw v1 SDL without the marker is rejected with `FEDERATION_V1_NOT_SUPPORTED`. Federation v2 schemas continue to use their `@link`; for a remote source, the optional exact `"2.0"` marker selects `_service.sdl` acquisition without enabling the v1 parser.
- **Several Apollo Federation v2 directives are not supported.** Composition rejects `@composeDirective`, `@authenticated`, `@requiresScopes`, and `@policy` with `FEDERATION_DIRECTIVE_NOT_SUPPORTED`. Remove the directive, or express the equivalent with a GraphQL Federation construct.
- **Enums used in input positions must define identical value sets across subgraphs.** When an Apollo Federation subgraph is part of the composition, enum value merging resolves to union, so enums used only in output positions compose even when their values diverge. Enums used in any input position must still agree exactly and are otherwise rejected with `ENUM_VALUES_MISMATCH`; Apollo's input-only intersection merge is not performed. The `enumValuesMergeBehavior` merger setting can force strict merging.

These error codes are listed in the [Composition log-code reference](../composition.md#log-codes-reference).

Feature support tracks the [GraphQL Hive federation-gateway-audit](https://github.com/graphql-hive/federation-gateway-audit) compliance suite, and the set of supported features grows as the connector passes more of that suite.

# Relationship to Migrating

The connector runs your Apollo Federation subgraphs as they are. It does not rewrite them. When you want to move a subgraph to the GraphQL Federation protocol (`[Lookup]` in place of `@key`, `[Require]` in place of `@requires`, and so on), see [Coming from Apollo Federation](../migration/coming-from-apollo-federation.md).

You do not have to choose one protocol for the whole graph. Apollo Federation and GraphQL Federation subgraphs compose together, so you can put the gateway in front of your existing Apollo Federation fleet first. Then you can move subgraphs to the GraphQL Federation protocol one at a time while the rest keep running unchanged.

# Next Steps

- Compose and run a gateway: [nitro fusion compose](../cli.md#nitro-fusion-compose) and [Getting Started](../getting-started.md).
- Understand entity resolution in the GraphQL Federation model: [Entities and Lookups](../entities-and-lookups.md).
- Extend interfaces across source schemas: [Interface Objects](../interface-objects.md).
- Move subgraphs to the GraphQL Federation protocol: [Coming from Apollo Federation](../migration/coming-from-apollo-federation.md).

---

### Website Content Docs Fusion Guides First Party Api (`website/content/docs/fusion/guides/first-party-api.md`)

---
title: "First-Party API"
description: "Deploy a Fusion gateway as a locked-down first-party GraphQL API using trusted documents so only pre-registered operations are executed."
---

When your GraphQL API is only consumed by clients you control — mobile apps, websites, internal services — there is no reason for the server to accept arbitrary GraphQL Operations at runtime. You already know every operation your clients will ever send, because you wrote them. Trusted documents let you take advantage of that fact.

The idea is simple: rather than shipping GraphQL operation text inside your client and sending it to the server on every request, you extract all operations at build time and register them with the server ahead of deployment. At runtime, clients reference operations by a short identifier instead of sending the full operation text. The server looks up the pre-registered operation, executes it, and returns the result. It never parses or validates an operation it has not seen before.

This eliminates entire classes of attacks. The GraphQL parser and validator never process untrusted input, so parser exploits, validation-level denial of service, and complexity attacks simply cannot reach your server. But the benefits go well beyond security. Trusted documents give you something that REST and gRPC cannot: field-level visibility into which client, in which version, uses which operations and which fields. Because every operation is registered with its client identity and version, you can make informed schema evolution decisions at the individual field level. You know exactly who will be affected before you deprecate or remove a field, enabling safe, incremental API evolution at scale.

This is why Meta, Netflix, and X operate GraphQL in production with trusted documents.

This guide covers the server side of this workflow: how to configure a Fusion gateway to only execute trusted, pre-registered operations.

# How trusted documents work

Trusted documents (also called persisted operations) turn your GraphQL endpoint from an open operation processor into a closed, auditable contract. The workflow has three steps:

1. **Extract.** The client compiler (Relay, Strawberry Shake, or similar) extracts every GraphQL operation from the codebase at build time. Each operation is hashed to produce a stable, unique identifier.
2. **Publish.** The extracted operations are published to the Nitro client registry before the application is deployed.
3. **Execute.** At runtime, clients send the operation hash instead of the full operation text. The gateway looks up the operation by ID in the client registry, executes the pre-registered document, and returns the result.

Once this workflow is in place, the gateway never parses an operation it has not seen before.

# Configure the Fusion gateway

The Fusion gateway does not need `UsePersistedOperationPipeline()` because the gateway pipeline already handles operation resolution by ID. You only need to connect the gateway to Nitro and enable the security options that block ad-hoc operations and skip document body parsing.

First, add the `ChilliCream.Nitro.Fusion` package to your gateway project:

```shell
dotnet add package ChilliCream.Nitro
dotnet add package ChilliCream.Nitro.Fusion
```

Then configure the gateway:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddNitro().AddDefaults();

builder
    .AddGraphQLGateway()
    .ModifyRequestOptions(o =>
    {
        o.PersistedOperations.OnlyAllowPersistedDocuments = true;
        o.PersistedOperations.AllowDocumentBody = false;
    });

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapGraphQL();
}

app.MapGraphQLPersistedOperations();

app.Run();
```

> [!NOTE]
> `AddDefaults()` is generated by a source generator in the `ChilliCream.Nitro` package. It requires the `ChilliCream.Nitro.Fusion` package to be referenced in your project. If `AddDefaults()` is not available, verify your package references or use the explicit `.AddFusion()` method instead. See the [Analyzers](../../nitro/analyzers.md) page for details.

`MapGraphQL()` is only registered in development. It exposes the standard GraphQL endpoint that client tooling needs: ad-hoc operations, introspection, and the schema endpoint at `/graphql/schema.graphql`. In production, only `MapGraphQLPersistedOperations()` is registered. The gateway exclusively accepts requests that reference a registered operation ID by URL, and the HTTP request parser skips reading the `query` field entirely.

# What the settings do

**`OnlyAllowPersistedDocuments = true`** means every executed operation must match a persisted document. Clients can still send the full operation text in the `query` field; the gateway will parse it, hash it, and verify that the hash matches a registered operation. This is useful during migration: legacy clients keep sending operation text while new clients send only the hash. Either way, only known operations execute.

**`AllowDocumentBody = false`** (this is the default) goes further. It configures the transport layer to skip parsing the `query` field entirely from JSON request bodies and GET query parameters. The GraphQL parser is never invoked for incoming request documents. This is the strict trusted documents mode.

**Together**, the gateway never parses untrusted GraphQL input. Operations are loaded from the client registry by ID, where they were pre-validated at publish time. The parser, validator, and cost analyzer are never exercised by external traffic.

# Client request format

With `MapGraphQLPersistedOperations()`, the operation hash and operation name are part of the URL path. Clients never send a `query` or `id` field. Only `variables` and `extensions` are specified, either in the POST body or as query parameters on GET:

**HTTP POST to `/graphql/persisted/{operationId}/{operationName}`:**

```json
{
  "variables": { "first": 10 }
}
```

**HTTP GET to `/graphql/persisted/{operationId}/{operationName}`:**

```http
GET /graphql/persisted/0c95d31ca29272475bf837f944f4e513/GetProducts?variables={"first":10}
```

The default base path is `/graphql/persisted`. You can customize it:

```csharp
app.MapGraphQLPersistedOperations("/api/operations");
```

You can require that clients always include an operation name in the URL:

```csharp
app.MapGraphQLPersistedOperations(requireOperationName: true);
```

The deterministic GET routes produce stable cache keys, which makes them ideal for CDN and HTTP response caching. See [Cache Control](../cache-control.md) for details on `@cacheControl` directives and response headers.

# Development workflow

During development, you need to iterate on operations without going through the full extract-publish-deploy cycle every time you change an operation. Tools like Nitro (the GraphQL IDE) need to send ad-hoc operations for exploration and testing.

Use an HTTP request interceptor to allow ad-hoc operations in development while keeping the production configuration locked down:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddNitro().AddDefaults();

builder
    .AddGraphQLGateway()
    .AddHttpRequestInterceptor<DevToolsInterceptor>()
    .ModifyRequestOptions(o =>
    {
        o.PersistedOperations.OnlyAllowPersistedDocuments = true;
        o.PersistedOperations.AllowDocumentBody = false;
    });

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapGraphQL();
}

app.MapGraphQLPersistedOperations();

app.Run();
```

The conditional `MapGraphQL()` in the configuration above already handles the endpoint side. For the execution pipeline, use an HTTP request interceptor to allow non-persisted operations in development:

```csharp
public class DevToolsInterceptor : DefaultHttpRequestInterceptor
{
    private readonly IHostEnvironment _env;

    public DevToolsInterceptor(IHostEnvironment env) => _env = env;

    public override ValueTask OnCreateAsync(
        HttpContext context,
        IRequestExecutor requestExecutor,
        OperationRequestBuilder requestBuilder,
        CancellationToken cancellationToken)
    {
        if (_env.IsDevelopment())
        {
            requestBuilder.AllowNonPersistedOperation();
        }

        return base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken);
    }
}
```

In production, `IsDevelopment()` returns `false`, and all requests must reference a registered operation ID. In development, the interceptor calls `AllowNonPersistedOperation()` on the request builder, which bypasses the `OnlyAllowPersistedDocuments` check for that request.

Replace the environment check with an authorization policy, API key, or other mechanism that fits your requirements. The important thing is that production traffic always goes through the persisted operation path.

# Putting it together

Here is a complete `Program.cs` for a Fusion gateway deployed as a first-party API with trusted documents, authentication, and persisted operation routes:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddHeaderPropagation(c =>
    {
        c.Headers.Add("GraphQL-Preflight");
        c.Headers.Add("Authorization");
    });

builder.Services
    .AddHttpClient("fusion")
    .AddHeaderPropagation();

builder.Services
    .AddAuthentication()
    .AddJwtBearer(options =>
    {
        options.Authority = "https://your-identity-provider.com/realms/your-realm";
        options.Audience = "graphql-api";
    });

builder.Services.AddAuthorization();

builder.Services.AddNitro().AddDefaults();

builder
    .AddGraphQLGateway()
    .AddHttpRequestInterceptor<DevToolsInterceptor>()
    .ModifyRequestOptions(o =>
    {
        o.PersistedOperations.OnlyAllowPersistedDocuments = true;
        o.PersistedOperations.AllowDocumentBody = false;
    });

var app = builder.Build();

app.UseHeaderPropagation();
app.UseAuthentication();
app.UseAuthorization();

if (app.Environment.IsDevelopment())
{
    app.MapGraphQL();
}

app.MapGraphQLPersistedOperations();

app.Run();
```

This configuration:

- Validates JWT tokens at the gateway and propagates the `Authorization` header to subgraphs.
- Rejects any request that does not reference a registered operation ID.
- Never parses the `query` field from incoming requests.
- Exposes deterministic GET routes at `/graphql/persisted/{operationId}` for CDN-friendly caching.
- Allows ad-hoc operations in development via the `DevToolsInterceptor`.

# Next Steps

- **"How do I extract and publish operations from my client?"** See [Client Registry](../../nitro/apis/client-registry.md) for extracting operations, publishing them to Nitro, and managing multiple client versions.
- **"I need authentication and authorization."** See [Authentication and Authorization](../authentication-and-authorization.md) for JWT validation, header propagation, and subgraph-level authorization.
- **"I need CDN and HTTP response caching."** See [Cache Control](../cache-control.md) for `@cacheControl` directives, composition merge behavior, and gateway response headers.
- **"I need to protect against complexity attacks on a public API."** See [Request Limits](../request-limits.md) for parser limits, validation limits, and planner guardrails.

---

### Website Content Docs Fusion Guides Third Party Api (`website/content/docs/fusion/guides/third-party-api.md`)

---
title: "Third-Party API"
description: "Draft guide for operating a Fusion gateway as a third-party GraphQL API, including open-client concerns, caching strategy, and safety controls."
---

This page is a draft.

Use this guide when your Fusion gateway is exposed to external or third-party clients that can send arbitrary queries.

Planned topics:

- Threat model and API posture for public GraphQL endpoints.
- Cache strategy for public traffic.
- Complexity and abuse protections.
- Authentication and authorization patterns.
- Operational guidance and rollout checklist.

---

### Website Content Docs Fusion Migration Coming From Apollo Federation (`website/content/docs/fusion/migration/coming-from-apollo-federation.md`)

---
title: "Migrate from Apollo Federation"
metaTitle: "Migrate from Apollo Federation to Hot Chocolate Fusion"
description: "Map Apollo Federation concepts like @key and _entities to Hot Chocolate Fusion equivalents such as [Lookup], and migrate subgraphs, gateway, and CI/CD."
---

If you have experience with Apollo Federation, you already understand the core idea behind distributed GraphQL: multiple services contribute types and fields to a single, unified schema, and a gateway coordinates query execution across them. HotChocolate Fusion supports two subgraph protocols: Apollo Federation and GraphQL Federation, the open [GraphQL Federation specification](https://graphql.github.io/composite-schemas-spec/) (formerly the GraphQL Composite Schemas specification). This guide is about moving a subgraph from the Apollo Federation protocol to the GraphQL Federation protocol.

This guide maps Apollo Federation concepts to their Fusion equivalents, explains behavioral differences, and walks you through migrating subgraphs, the gateway, and your CI/CD pipeline. It is self-contained: you can complete a migration by following this guide alone. Links to other Fusion docs pages provide deeper context, not prerequisites.

> [!NOTE]
> Prefer to keep running your existing Apollo Federation subgraphs unchanged behind the Fusion gateway, rather than moving them to the GraphQL Federation protocol? See the [Apollo Federation connector](../connectors/apollofederation.md). The two protocols interoperate, so you can move one subgraph at a time.

# Concept Mapping

The table below maps Apollo Federation concepts to their Fusion equivalents. Some are straightforward renames; others involve meaningful behavioral changes. The "Key Difference" column flags which is which.

| Apollo Federation                                | HotChocolate Fusion                               | Key Difference                                                                                         |
| ------------------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `@key(fields: "id")`                             | `[Lookup]` on a Query field                       | Fusion uses explicit, typed lookup fields instead of implicit `_entities`. No `@key` directive needed. |
| `__resolveReference` / `_entities` query         | Regular Query fields with `[Lookup]`              | Lookups are real fields you can call and test directly.                                                |
| `@external`                                      | `[External]` (rarely needed)                      | Same concept, but less frequently needed in Fusion.                                                    |
| `@requires(fields: "...")` on a field            | `[Require("...")]` on an argument                 | Argument-level, not field-level. Required arguments are hidden from the composed schema.               |
| `@provides(fields: "...")`                       | `[Provides("...")]` / `[Parent(requires: "...")]` | Same optimization concept.                                                                             |
| `@shareable`                                     | `[Shareable]`                                     | Same concept and semantics. Key fields are automatically shareable.                                    |
| `@override(from: "...")`                         | `[Override(from: "...")]`                         | Same concept.                                                                                          |
| `@inaccessible`                                  | `[Inaccessible]`                                  | Same concept.                                                                                          |
| `@tag`                                           | `[Tag]`                                           | Same concept.                                                                                          |
| Apollo Router / Gateway                          | Fusion Gateway (`AddGraphQLGateway()`)            | A .NET ASP.NET Core app, not a separate binary.                                                        |
| `rover` CLI                                      | Nitro CLI (`nitro fusion ...`)                    | Schema composition, validation, and delivery.                                                          |
| GraphOS managed federation                       | Nitro cloud or local CI/CD composition            | Build-time composition. Works fully offline.                                                           |
| Supergraph schema (SDL)                          | Composed schema + `.far` archive                  | Binary archive containing the composed schema and subgraph metadata.                                   |
| Federation subgraph library (`@apollo/subgraph`) | No equivalent needed                              | Subgraphs are standard HotChocolate servers. No Apollo Federation library.                             |
| `#!graphql _service { sdl }` introspection       | `#!shell dotnet run -- schema export`             | Schema export is a CLI command, not a runtime introspection field.                                     |

# What Fusion Does Not Need

Several things from Apollo's model have no Fusion equivalent because the architecture handles them differently.

**No `_entities` query.** In Apollo Federation, the gateway resolves entities by calling a hidden `_entities` root field with typed representations. In Fusion, entity resolution happens through regular Query fields annotated with `[Lookup]`. These are real, typed fields that you can call directly from any GraphQL client for testing and debugging.

**No `__resolveReference` resolvers.** In Apollo Federation, every subgraph that contributes to an entity must implement a `__resolveReference` function. In Fusion, you write a normal Query field (like `GetProductById`) and add `[Lookup]`. The gateway calls this field like any other query.

**No Apollo Federation subgraph library.** Apollo Federation subgraphs require `@apollo/subgraph` (or the equivalent in your language) to add Apollo Federation-specific fields and middleware. Fusion subgraphs are standard HotChocolate servers. You add a few attributes (`[Lookup]`, `[Shareable]`, etc.) and export the schema. There is no special Apollo Federation runtime.

**No `_service` introspection.** Apollo Federation subgraphs expose their SDL via `#!graphql _service { sdl }`. Fusion subgraphs export their schema as a `.graphqls` file using the command `#!shell dotnet run -- schema export`. The schema file and its companion `schema-settings.json` are what composition reads.

**No `@key` directive.** In Apollo Federation, `#!sdl @key(fields: "id")` tells the gateway which fields identify an entity. In Fusion, the gateway infers entity keys from the arguments of your `[Lookup]` fields. If your lookup is `GetProductById(int id)`, the gateway knows that `id` is the key for `Product`. You can use `#!csharp [EntityKey("id")]` for explicit key declaration when needed, but it is rarely necessary.

# Behavioral Differences in Depth

Beyond naming, several concepts work fundamentally differently in Fusion. Understanding these differences is important for a successful migration.

## Entity Resolution: Lookups vs. the Entities Query

This is the most significant architectural difference between Apollo Federation and the GraphQL Federation protocol.

**Apollo Federation approach:** The gateway sends a batch request to the `_entities` field, passing an array of typed representations (like `#!graphql { __typename: "Product", id: "1" }`). Each subgraph's `__resolveReference` function handles these representations.

```graphql
# Apollo: hidden _entities query (you never write this yourself)
query {
  _entities(representations: [{ __typename: "Product", id: "1" }]) {
    ... on Product {
      name
      price
    }
  }
}
```

**Fusion approach:** The gateway calls a regular Query field that you define and annotate with `[Lookup]`. There is no hidden protocol.

```csharp
// Fusion: a regular query field with [Lookup]
[QueryType]
public static partial class ProductQueries
{
    [Lookup]
    public static async Task<Product?> GetProductById(
        int id,
        IProductByIdDataLoader productById,
        CancellationToken cancellationToken)
        => await productById.LoadAsync(id, cancellationToken);
}
```

```graphql
# Fusion: the gateway calls a regular query
query {
  productById(id: 1) {
    name
    price
  }
}
```

The practical benefit is direct testing. You can call `productById` in your GraphQL IDE (like Nitro / Banana Cake Pop) to test entity resolution. In Apollo Federation, `_entities` is hidden and awkward to test manually.

**Multiple lookups per entity.** In Apollo Federation, an entity can have multiple `@key` directives to support different keys. In Fusion, you define multiple `[Lookup]` fields, one per key:

```csharp
[QueryType]
public static partial class UserQueries
{
    [Lookup]
    public static async Task<User?> GetUserById(
        int id,
        IUserByIdDataLoader userById,
        CancellationToken cancellationToken)
        => await userById.LoadAsync(id, cancellationToken);

    [Lookup]
    public static async Task<User?> GetUserByUsername(
        string username,
        IUserByNameDataLoader userByName,
        CancellationToken cancellationToken)
        => await userByName.LoadAsync(username, cancellationToken);
}
```

The gateway automatically discovers all available lookups and uses whichever one has the keys it needs.

**Internal lookups.** When a subgraph extends an entity from another subgraph, it needs a lookup the gateway can use for entity resolution, but you may not want that lookup exposed to clients. In Apollo Federation, `_entities` handles this implicitly. In Fusion, you mark the lookup with `[Internal]`:

```csharp
// In the Reviews subgraph: an internal-only lookup for Product
[QueryType]
public static partial class ProductQueries
{
    [Lookup, Internal]
    public static Product GetProductById([ID<Product>] int id)
        => new(id);
}
```

The `[Internal]` attribute hides this field from the composed schema. Only the gateway uses it during query planning.

For more on lookups and entity resolution patterns, see [Entities and Lookups](../entities-and-lookups.md).

## Requirements Operate on Arguments, Not Fields

In Apollo Federation, `@requires` is a field-level directive. It declares that a field depends on data from another subgraph:

```graphql
# Apollo: @requires on the field
type Product @key(fields: "id") {
  id: ID!
  weight: Float @external
  shippingEstimate: Int @requires(fields: "weight")
}
```

In Fusion, `[Require]` is an argument-level attribute. The required data arrives as a method parameter, and that parameter is hidden from the composed schema:

```csharp
// Fusion: [Require] on the argument
[EntityKey("id")]
public sealed record Product([property: ID<Product>] int Id)
{
    public int GetDeliveryEstimate(
        string zip,
        [Require("""
            {
              weight,
              length: dimension.length,
              width: dimension.width,
              height: dimension.height
            }
            """)]
        ProductDimensionInput dimension)
    {
        // dimension.Weight, dimension.Length, etc. are provided by the gateway
        // from whatever subgraph owns those fields
        var volume = dimension.Length * dimension.Width * dimension.Height;
        return CalculateEstimate(zip, volume, dimension.Weight);
    }
}
```

In the composed schema, clients see `#!sdl deliveryEstimate(zip: String!)`. The `dimension` parameter is invisible. The gateway resolves `weight` and `#!graphql dimension { length width height }` from the owning subgraph and passes them to the Shipping subgraph automatically.

This changes how you design resolvers. In Apollo, the required data is available on `this` (the entity object). In Fusion, it arrives as a typed argument, which makes the dependency explicit and testable.

Cross-subgraph data dependencies with `[Require]` are also covered in the [Adding a Subgraph](../adding-a-subgraph.md) page. A dedicated deep-dive will be available in future documentation.

## Entity Stubs: How Subgraphs Reference Foreign Entities

In Apollo Federation, when a subgraph extends an entity from another subgraph, it uses `extend type` with `@key`:

```graphql
# Apollo: Reviews subgraph extending Product
extend type Product @key(fields: "id") {
  id: ID! @external
  reviews: [Review!]!
}
```

In Fusion, you create an **entity stub**: a minimal C# type that declares the entity's key and adds your fields:

```csharp
// Fusion: Reviews subgraph's entity stub for Product
public sealed record Product([property: ID<Product>] int Id)
{
    [UsePaging(ConnectionName = "ProductReviews")]
    public async Task<Connection<Review>> GetReviewsAsync(
        PagingArguments pagingArgs,
        IReviewsByProductIdDataLoader reviewByProductId,
        CancellationToken cancellationToken)
        => await reviewByProductId
            .With(pagingArgs)
            .LoadAsync(Id, cancellationToken)
            .ToConnectionAsync();
}
```

The stub is not a copy of the full Product type. It only declares the key (`Id`) and the fields this subgraph contributes (`reviews`). The gateway merges it with the full `Product` type from the Products subgraph during composition.

## Composition: Build Step, Not Cloud Operation

In the Apollo Federation workflow, composition typically happens in GraphOS cloud when you run `rover subgraph publish`. The router downloads the composed supergraph schema from GraphOS at startup.

In Fusion, composition is a local build step you run on your machine or in CI:

```shell
nitro fusion compose \
  --source-schema-file ./products/schema.graphqls \
  --source-schema-file ./reviews/schema.graphqls \
  --archive gateway.far
```

This produces a `.far` (Fusion Archive) file. The archive contains the composed schema and subgraph metadata. You can inspect what composition produced, run it locally, and validate it in CI before deployment.

You can also use Nitro cloud for managed composition (similar to Apollo's GraphOS), but it is not required. Everything works fully offline.

For more on composition rules and error resolution, see [Composition](../composition.md).

## The Gateway Is Code, Not a Separate Binary

Apollo Router is a standalone binary (written in Rust) that you configure via YAML. Fusion's gateway is an ASP.NET Core application that you write and control:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far");

var app = builder.Build();
app.MapGraphQL();
app.Run();
```

Because it is a standard ASP.NET Core app, you get full access to the middleware pipeline, dependency injection, authentication, header propagation, and everything else in the .NET ecosystem. There is no separate binary to deploy or configure.

# Step-by-Step Migration

## Phase 1: Migrate Subgraphs

For each Apollo Federation subgraph, follow these steps.

### Step 1: Replace Apollo Packages with HotChocolate

Remove the Apollo Federation subgraph library and add HotChocolate packages.

**Apollo Federation (Node.js / TypeScript):**

```json
{
  "dependencies": {
    "@apollo/subgraph": "^2.x",
    "graphql": "^16.x"
  }
}
```

**Fusion (C#):**

```xml
<Project Sdk="Microsoft.NET.Sdk.Web">
  <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>
```

Set up a standard HotChocolate server in `Program.cs`:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder
    .AddGraphQL("my-subgraph")
    .AddTypes()
    .AddMutationConventions()
    .AddGlobalObjectIdentification();

var app = builder.Build();
app.MapGraphQL();
app.RunWithGraphQLCommands(args);
```

The call to `RunWithGraphQLCommands(args)` enables `#!shell dotnet run -- schema export`, which is how Fusion exports the subgraph schema for composition.

### Step 2: Convert Entity Resolution to Lookups

This is the core conversion. For every entity type that has a `@key` directive and a `__resolveReference` resolver, create a `[Lookup]` query field.

**Apollo Federation (TypeScript):**

```typescript
// Apollo: schema
// type Product @key(fields: "id") {
//   id: ID!
//   name: String!
//   price: Float!
// }

const resolvers = {
  Product: {
    __resolveReference(ref) {
      return fetchProductById(ref.id);
    },
  },
  Query: {
    product: (_, { id }) => fetchProductById(id),
  },
};
```

**Fusion (C#):**

```csharp
// The entity type
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }
}

// The lookup replaces both __resolveReference AND the query field
[QueryType]
public static partial class ProductQueries
{
    [Lookup]
    public static async Task<Product?> GetProductById(
        int id,
        IProductByIdDataLoader productById,
        CancellationToken cancellationToken)
        => await productById.LoadAsync(id, cancellationToken);
}
```

The `[Lookup]` attribute serves double duty: it makes the field callable by clients (like a normal query) and tells the gateway to use it for entity resolution. If you only want the gateway to use it, add `[Internal]`:

```csharp
[Lookup, Internal]
public static Product GetProductById(int id) => new(id);
```

**Key point:** You do not need a `@key` directive. The gateway infers the entity key from the lookup's arguments. If your lookup takes `int id`, the gateway knows `id` is the key.

### Step 3: Convert Field Requirements

Replace field-level `@requires` with argument-level `[Require]`.

**Apollo Federation (GraphQL SDL):**

```graphql
type Product @key(fields: "id") {
  id: ID!
  weight: Float @external
  shippingEstimate: Int @requires(fields: "weight")
}
```

```typescript
const resolvers = {
  Product: {
    shippingEstimate(product) {
      // product.weight is available because of @requires
      return calculateEstimate(product.weight);
    },
  },
};
```

**Fusion (C#):**

```csharp
[EntityKey("id")]
public sealed record Product([property: ID<Product>] int Id)
{
    public int GetShippingEstimate(
        [Require("weight")] int weight)
    {
        return CalculateEstimate(weight);
    }
}
```

The `weight` parameter is hidden from the composed schema. Clients call `shippingEstimate` with no arguments. The gateway resolves `weight` from whichever subgraph owns it and passes it to this resolver.

For complex requirements that map multiple fields into an input object:

```csharp
public int GetDeliveryEstimate(
    string zip,
    [Require("""
        {
          weight,
          length: dimension.length,
          width: dimension.width,
          height: dimension.height
        }
        """)]
    ProductDimensionInput dimension)
{
    // Use dimension.Weight, dimension.Length, etc.
}
```

### Step 4: Convert External Fields and Provides

**`@external`** has a direct equivalent in `[External]`, but it is less frequently needed. In Apollo Federation, you must mark any field referenced by `@requires` as `@external`. In Fusion, the `[Require]` selection syntax references fields from the composed graph directly, so no `@external` annotation is needed on the entity type.

**`@provides`** maps to `[Parent(requires: "...")]`. This optimization hint tells the gateway that a field can resolve certain nested fields locally, avoiding an extra subgraph call:

**Apollo:**

```graphql
type Review {
  author: User @provides(fields: "email")
}

type User @key(fields: "id") {
  id: ID!
  email: String! @external
}
```

**Fusion:**

```csharp
[ObjectType<Review>]
public static partial class ReviewNode
{
    public static User GetAuthor(
        [Parent(requires: nameof(Review.AuthorId))] Review review)
        => new User(review.AuthorId, review.AuthorEmail);
}
```

### Step 5: Handle Shared Fields

`[Shareable]` works the same in both systems. If multiple subgraphs define the same field on the same type, each definition must be marked as shareable.

**Apollo (GraphQL SDL):**

```graphql
# Both subgraphs define User.name
type User @key(fields: "id") {
  id: ID!
  name: String! @shareable
}
```

**Fusion (C#):**

```csharp
[ObjectType<User>]
public static partial class UserNode
{
    [Shareable]
    public static string GetName([Parent] User user) => user.Name!;
}
```

One difference: in Fusion, key fields (like `id`) are automatically shareable. You do not need to annotate them.

### Step 6: Create Schema Settings

Every Fusion subgraph needs a `schema-settings.json` file that tells composition where the subgraph lives and how to connect to it:

```json
{
  "name": "products-api",
  "transports": {
    "http": {
      "clientName": "fusion",
      "url": "{{API_URL}}"
    }
  },
  "environments": {
    "development": {
      "API_URL": "http://localhost:5100/graphql"
    },
    "production": {
      "API_URL": "https://products.example.com/graphql"
    }
  }
}
```

Place this file next to your project. The `name` field must be unique across all subgraphs. The `clientName` field (`"fusion"`) must match the named HTTP client configured in the gateway.

### Step 7: Export the Schema

Run the schema export command:

```shell
dotnet run -- schema export
```

This generates a `.graphqls` file containing your subgraph's schema with Fusion-specific directives. This file, together with `schema-settings.json`, is what composition reads.

## Phase 2: Migrate the Gateway

Replace Apollo Router with a Fusion gateway ASP.NET Core project.

### Step 1: Create the Gateway Project

```shell
dotnet new web -n Gateway
cd Gateway
dotnet add package HotChocolate.Fusion.AspNetCore
```

### Step 2: Configure the Gateway

**Minimal gateway (`Program.cs`):**

```csharp
var builder = WebApplication.CreateBuilder(args);

// Register the Fusion gateway
builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far");

var app = builder.Build();
app.MapGraphQL();
app.Run();
```

This loads the gateway configuration from a local `.far` file. To use Nitro cloud for configuration delivery instead (similar to Apollo's managed composition in GraphOS):

```csharp
builder
    .AddGraphQLGateway()
    .AddNitro();
```

### Step 3: Set Up Header Propagation

If your Apollo Router forwards headers (like `Authorization`) to subgraphs, configure the same in Fusion:

**Apollo Router (YAML):**

```yaml
headers:
  all:
    request:
      - propagate:
          named: Authorization
```

**Fusion Gateway (C#):**

```csharp
var builder = WebApplication.CreateBuilder(args);

// Configure header propagation
builder.Services.AddHeaderPropagation(options =>
{
    options.Headers.Add("Authorization");
});

// The named HTTP client must match "clientName" in schema-settings.json
builder.Services
    .AddHttpClient("fusion")
    .AddHeaderPropagation();

builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far");

var app = builder.Build();
app.UseHeaderPropagation();
app.MapGraphQL();
app.Run();
```

### Step 4: Compose and Run

Compose your subgraph schemas into a gateway archive:

```shell
nitro fusion compose \
  --source-schema-file ./products/schema.graphqls \
  --source-schema-file ./reviews/schema.graphqls \
  --archive gateway.far
```

Then start the gateway:

```shell
cd Gateway
dotnet run
```

Navigate to `http://localhost:5000/graphql` to open the Nitro IDE and run cross-subgraph queries.

## Phase 3: Migrate CI/CD

Replace Apollo's `rover` commands with Nitro CLI equivalents.

| Apollo (`rover`)           | Fusion (`nitro`)                               |
| -------------------------- | ---------------------------------------------- |
| `rover subgraph check`     | `nitro fusion validate`                        |
| `rover subgraph publish`   | `nitro fusion upload` + `nitro fusion publish` |
| `rover supergraph compose` | `nitro fusion compose`                         |

### Schema Upload (Replaces Rover Subgraph Publish)

In the Apollo workflow, publishing a subgraph triggers server-side composition. In Fusion, this is a two-step process: upload the schema, then publish to trigger composition.

**Apollo:**

```shell
rover subgraph publish my-graph@production \
  --name products \
  --schema ./schema.graphqls \
  --routing-url https://products.example.com/graphql
```

**Fusion:**

```shell
# Step 1: Upload the source schema
nitro fusion upload \
  --source-schema-file ./schema.graphqls \
  --tag v1.0.0 \
  --api-id $NITRO_API_ID \
  --api-key $NITRO_API_KEY

# Step 2: Publish to trigger composition and deploy to a stage
nitro fusion publish \
  --source-schema products-api@v1.0.0 \
  --stage production \
  --api-id $NITRO_API_ID \
  --api-key $NITRO_API_KEY
```

### Schema Validation (Replaces Rover Subgraph Check)

**Apollo:**

```shell
rover subgraph check my-graph@production \
  --name products \
  --schema ./schema.graphqls
```

**Fusion:**

```shell
nitro fusion validate \
  --source-schema-file ./schema.graphqls \
  --stage production \
  --api-id $NITRO_API_ID \
  --api-key $NITRO_API_KEY
```

### Local Composition (Replaces Rover Supergraph Compose)

**Apollo:**

```shell
rover supergraph compose --config ./supergraph-config.yaml --output supergraph.graphql
```

**Fusion:**

```shell
nitro fusion compose \
  --source-schema-file ./products/schema.graphqls \
  --source-schema-file ./reviews/schema.graphqls \
  --archive gateway.far
```

### Example GitHub Actions Workflow

```yaml
name: Deploy Subgraph
on:
  push:
    branches: [main]
    paths:
      - "src/Products/**"

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 10.0.x

      - name: Build and publish container
        working-directory: src/Products
        run: dotnet publish -c Release

      - name: Export schema
        working-directory: src/Products
        run: dotnet run -- schema export

      - name: Upload source schema
        run: |
          nitro fusion upload \
            --source-schema-file src/Products/schema.graphqls \
            --tag ${{ github.sha }} \
            --api-id ${{ secrets.NITRO_API_ID }} \
            --api-key ${{ secrets.NITRO_API_KEY }}

      - name: Publish to production
        run: |
          nitro fusion publish \
            --source-schema products-api@${{ github.sha }} \
            --stage production \
            --api-id ${{ secrets.NITRO_API_ID }} \
            --api-key ${{ secrets.NITRO_API_KEY }}
```

For more on deployment workflows, see [Deployment and CI/CD](../deployment-and-ci-cd.md).

# Mindset Shifts

If you have spent significant time with Apollo Federation, some habits need adjusting. These are not only naming differences. They change how you think about your graph.

## Entity Resolution Is Explicit and Testable

In Apollo Federation, entity resolution happens through a hidden protocol (`_entities` + `__resolveReference`). You cannot easily call `_entities` from a GraphQL client to debug resolution issues. In Fusion, entity resolution is a regular query field. You can open your IDE, call `#!graphql productById(id: 1)`, and see exactly what your lookup returns. This makes debugging straightforward.

## You Don't Need to Think About Entity Ownership the Same Way

Apollo Federation has a strong concept of entity "ownership": one subgraph is the "defining" subgraph for an entity, and others "extend" it. In Fusion, all subgraphs contribute fields to shared entity types. The gateway uses lookups to resolve entities wherever they need to be fetched. The question is not "who owns this entity?" but "which subgraphs provide lookups for it?"

## Composition Is a Build Step, Not a Cloud Operation

In the Apollo workflow, composition typically happens in GraphOS when you publish a subgraph. In Fusion, composition is a command you run locally or in CI:

```shell
nitro fusion compose --archive gateway.far
```

You can run this on your machine, see the output, inspect errors, and fix them before pushing. There is no cloud service in the loop unless you choose to use Nitro cloud.

## Requirements Operate on Arguments, Not Fields

This changes resolver design. In Apollo Federation, required data appears on the entity object. In Fusion, it arrives as a method parameter:

```csharp
// The 'weight' parameter is injected by the gateway
public int GetShippingEstimate([Require("weight")] int weight)
{
    return CalculateEstimate(weight);
}
```

This makes dependencies explicit in the method signature. You can see exactly what data a resolver needs by looking at its parameters.

## The Gateway Is Your Code

Apollo Router is a pre-built binary you configure externally. Fusion's gateway is an ASP.NET Core application you control. You write `Program.cs`, configure middleware, add authentication, and deploy it like any other .NET service. This means you have full control but also full responsibility for the gateway's behavior.

# What Gets Simpler

Moving to the GraphQL Federation protocol resolves several common pain points from Apollo Federation.

**No Apollo Federation library required.** Apollo Federation subgraphs need `@apollo/subgraph` (or an equivalent library in your language). Fusion subgraphs are standard HotChocolate servers. If you already have a HotChocolate GraphQL server, it is already a valid Fusion subgraph. Add a few attributes and export the schema.

**Lookups are testable.** In Apollo Federation, `_entities` is hidden and awkward to test. In Fusion, lookups are regular query fields. You can write integration tests that call them directly, use them in your GraphQL IDE, and verify their behavior in isolation.

**Build-time composition catches errors early.** GraphOS composes Apollo Federation schemas when you publish. If composition fails, you find out after pushing. Fusion's composition runs locally as a build step, so you can catch schema conflicts the same way you catch compilation errors: before you commit.

**.NET-native tooling.** If your team is a .NET shop, Fusion means your gateway, subgraphs, and tooling are all .NET. No Node.js dependency for the gateway or CLI, no context-switching between languages.

**Open standards.** Fusion implements the [GraphQL Federation specification](https://graphql.github.io/composite-schemas-spec/), an open, vendor-neutral standard under the GraphQL Foundation. Your subgraph schemas are portable. They are not locked into any vendor's directive syntax.

**Simpler subgraph setup.** Without an Apollo Federation library, there are fewer moving parts. A minimal Fusion subgraph is a HotChocolate server with `[Lookup]` on its entity query fields and a `schema-settings.json` file. That is the entire GraphQL Federation surface area.

# FAQ

## Can I migrate incrementally, with some subgraphs on Apollo Federation and some on GraphQL Federation?

Yes. A Fusion gateway composes and executes both subgraph protocols in one graph, so you can move subgraphs from the Apollo Federation protocol to the GraphQL Federation protocol one at a time while the rest keep running unchanged. To keep running your existing Apollo Federation subgraphs as they are, see the [Apollo Federation connector](../connectors/apollofederation.md). Convert and test each subgraph before moving on to the next.

## Do I need Nitro cloud?

No. Nitro cloud provides managed composition and gateway configuration delivery (similar to Apollo's GraphOS), but everything works without it. You can compose schemas locally with `nitro fusion compose`, load the `.far` file from disk with `AddFileSystemConfiguration()`, and never touch a cloud service. Nitro cloud is optional for teams that want managed schema delivery.

## What About Apollo Federation Auth Directives?

Fusion uses standard ASP.NET Core authentication and authorization. You configure JWT/cookie authentication in the gateway's middleware pipeline and use HotChocolate's `[Authorize]` attribute on fields and types in your subgraphs. There is no Fusion-specific auth directive. You use the same patterns you already know from ASP.NET Core.

## Can Fusion handle subscriptions?

Yes. Fusion supports real-time subscriptions through the gateway via SSE (Server-Sent Events) and WebSocket transports. Subgraphs can use any HotChocolate subscription provider, including Postgres-backed subscriptions for multi-instance scenarios.

## Where do I go from here?

After migrating, these pages provide deeper coverage of specific topics:

- [Getting Started](../getting-started.md): The full tutorial for building a Fusion setup from scratch
- [Entities and Lookups](../entities-and-lookups.md): Deep dive into entity resolution patterns
- [Adding a Subgraph](../adding-a-subgraph.md): Adding subgraphs with `[Require]` for cross-subgraph data
- [Composition](../composition.md): Composition rules, merging behavior, and error reference
- [Deployment and CI/CD](../deployment-and-ci-cd.md): Production deployment and pipeline setup
- [Nitro CLI Reference](../cli.md): Complete CLI command reference

---

### Website Content Docs Fusion Migration Migrate From 15 To 16 (`website/content/docs/fusion/migration/migrate-from-15-to-16.md`)

---
title: Migrate Hot Chocolate Fusion from 15 to 16
description: "Migration guide for Hot Chocolate Fusion v15 to v16: publish dual archive formats per subgraph, then cut the gateway over to the new `.far` configuration."
---

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

# Migration at a glance

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

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

# Per subgraph migration



## Migrate subgraph-config.json

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

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

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

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

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

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

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

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

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

### Environment-specific configuration

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

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

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

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

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

## Update subgraph

### Batch resolvers

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

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

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

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

### Transport batching

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

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

### GraphQL errors

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

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

---

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

## Migrate pipelines

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

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

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

Two pipelines in a typical subgraph repository need this change:

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

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

### Deployment pipeline

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

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

Below is the existing v15 pipeline for reference:

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

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

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

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

<PipelineChoiceTabs>
<PipelineChoiceTabs.GitHubAction>

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

</PipelineChoiceTabs.GitHubAction>
<PipelineChoiceTabs.AzureDevOps>

```yaml
- task: NitroFusionUpload@16
  inputs:
    # Authentication depends on the method you configured.
    # This example uses a Nitro service connection.
    nitroServiceConnection: <service-connection>
    apiId: <api-id>
    tag: <tag>
    sourceSchemaFiles: |
      ./src/SubgraphA/schema.graphql
```

</PipelineChoiceTabs.AzureDevOps>
<PipelineChoiceTabs.CLI>

```shell
dotnet nitro fusion upload \
  --tag "<tag>" \
  --api-id "<api-id>" \
  --api-key "<api-key>" \
  --source-schema-file "./src/SubgraphA/schema.graphql"
```

</PipelineChoiceTabs.CLI>
</PipelineChoiceTabs>

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

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

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



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

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

<PipelineChoiceTabs>
<PipelineChoiceTabs.GitHubAction>

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

</PipelineChoiceTabs.GitHubAction>
<PipelineChoiceTabs.AzureDevOps>

```yaml
- task: NitroFusionPublish@16
  inputs:
    # Authentication depends on the method you configured.
    # This example uses a Nitro service connection.
    nitroServiceConnection: <service-connection>
    apiId: <api-id>
    tag: <tag>
    stage: <stage>
    legacyV1Archive: ./gateway.fgp
    sourceSchemas: |
      subgraph-a
```

</PipelineChoiceTabs.AzureDevOps>
<PipelineChoiceTabs.CLI>

```shell
dotnet nitro fusion publish \
  --tag "<tag>" \
  --stage "<stage>" \
  --api-id "<api-id>" \
  --api-key "<api-key>" \
  --source-schema "subgraph-a" \
  --legacy-v1-archive "./gateway.fgp"
```

</PipelineChoiceTabs.CLI>
</PipelineChoiceTabs>

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

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

### PR validation pipeline

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

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

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

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

<PipelineChoiceTabs>
<PipelineChoiceTabs.GitHubAction>

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

</PipelineChoiceTabs.GitHubAction>
<PipelineChoiceTabs.AzureDevOps>

```yaml
- task: NitroFusionValidate@16
  inputs:
    # Authentication depends on the method you configured.
    # This example uses a Nitro service connection.
    nitroServiceConnection: <service-connection>
    apiId: <api-id>
    stage: <stage>
    legacyV1Archive: ./gateway.fgp
    sourceSchemaFiles: |
      ./src/SubgraphA/schema.graphql
```

</PipelineChoiceTabs.AzureDevOps>
<PipelineChoiceTabs.CLI>

```shell
dotnet nitro fusion validate \
  --stage "<stage>" \
  --api-id "<api-id>" \
  --api-key "<api-key>" \
  --legacy-v1-archive "./gateway.fgp" \
  --source-schema-file "./src/SubgraphA/schema.graphql"
```

</PipelineChoiceTabs.CLI>
</PipelineChoiceTabs>

## Migrate subgraph to v16

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

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

### Apply the source schema defaults

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

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

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

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

### Remove the legacy compatibility mode

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

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

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

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

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

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

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

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

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

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

# Upgrade the gateway

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

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

## Breaking changes

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

### AddFusionGatewayServer renamed to AddGraphQLGatewayServer

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

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

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

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

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

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

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

### ModifyFusionOptions split into ModifyOptions, ModifyRequestOptions and ModifyPlannerOptions

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

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

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

### Internal directives hidden from schema endpoint

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

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

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

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

### Cache configuration

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

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

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

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

### Document hash provider configuration

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

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

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

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

### Eager initialization by default

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

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

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

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

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

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

### Server options now configured via ModifyServerOptions

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

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

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

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

### Request batching is now disabled by default

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

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

If you were relying on request batching, you need to explicitly enable it:

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

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

### Configuration provider API

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

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

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

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

#### RegisterGatewayConfiguration → AddConfigurationProvider

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

#### ConfigureFromFile → AddFileSystemConfiguration

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

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

#### ConfigureFromDocument → AddInMemoryConfiguration

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

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

### Nitro integration

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

The key changes for Fusion projects:

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

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

**Before**

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

**After**

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

builder.Services.AddGraphQLGatewayServer();
```

### Diagnostic listener API redesigned

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

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

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

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

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

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

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

#### Scoped duration and error hooks

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

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

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

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

### IRequestContext

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

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

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

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

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

### Clearer separation between schema and application services

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

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

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

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

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

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

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

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

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

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

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

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

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



### Experimental @semanticNonNull support removed

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

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

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

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

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

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

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

**Schema snapshot tests**

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

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

**Downloading the schema from the gateway**

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

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

## Behavioral breaking changes

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

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

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

### Generated subgraph operation names changed

The gateway names every operation it sends to a source schema, and that name is what a subgraph records in its own logs and traces. In v15 the name was the client operation name followed by a counter that incremented once per subgraph request:

```text
GetProductReviews_1
```

In v16 the name carries a short hash of the client operation document, and the trailing number is the planner's step ID rather than a counter:

```text
GetProductReviews_94a407d5_1
```

The short hash is the first eight characters of the client operation document hash, which is MD5 in hex by default and therefore changes if you configure a different [document hash provider](#document-hash-provider-configuration). Characters that a GraphQL name cannot hold are rendered as underscores, so a hash in URL-safe Base64 has each `-` replaced in the name while the hash itself is unchanged. One request has one short hash, shared by every step of its main operation. When the client sends the document as source text, the hash is computed over those bytes as received rather than over a normalized form, so whitespace and comments change it. A request that carries a document ID or its own hash, such as a persisted operation, reuses the stored or supplied value instead.

The step ID is assigned by the operation planner and is also the ID of the step in the operation plan, so a subgraph log line points back at the exact plan step that issued it. Because the planner assigns IDs over the plan it selects, a composition change can renumber the steps even when the client document is unchanged.

Three further shapes replace their v15 equivalents:

- An anonymous client operation produces `Op_<shortHash>_<stepId>` instead of v15's `fetch_<rootFieldNames>_<counter>`.
- An incremental plan produced by `@defer` uses the literal `defer` in place of the short hash, for example `Op_defer_1`.
- With subgraph alias batching enabled, the merged operation is named `<name>_<shortHash>_Batch_<compositionHash>`, or `Op_<shortHash>_Batch_<compositionHash>` for an anonymous client operation, where the trailing 16 hex digits identify the composition of the batch.

Update any subgraph log parsing, dashboard grouping, or alert that matched the v15 name shape.

## Noteworthy changes

### Concurrent execution gate

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

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

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

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

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

### Parser limits

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

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

### RunWithGraphQLCommandsAsync returns exit code

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

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

### OpenTelemetry instrumentation for the gateway

v16 adds a `HotChocolate.Fusion.Diagnostics` package that instruments the gateway itself. It is not pulled in by `HotChocolate.Fusion.AspNetCore`, so add the reference explicitly:

```xml
<PackageReference Include="HotChocolate.Fusion.Diagnostics" Version="16.x.x" />
```

Then enable it on the gateway builder and register its activity source with OpenTelemetry:

```csharp
builder.Services
    .AddGraphQLGatewayServer()
    .AddInstrumentation();

builder.Services
    .AddOpenTelemetry()
    .WithTracing(tracing => tracing.AddHotChocolateFusionInstrumentation());
```

Spans and attributes follow the same conventions as the Hot Chocolate server. If you instrumented the v15 gateway through the underlying request executor, review [OpenTelemetry span and status changes](../../hotchocolate/migrating/migrate-from-15-to-16.md#opentelemetry-span-and-status-changes) for the renamed spans and attributes before updating dashboards.

Two spans carry the gateway attributes:

| Span                         | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GraphQL Operation Planning` | Covers planning the operation. Carries `graphql.processing.type=plan`.                                                                                                                                                                                                                                                                                                                                                                                                  |
| `GraphQL Step Execution`     | One span per execution node. Carries `graphql.processing.type=step_execute`, `graphql.operation.step.id`, `graphql.operation.step.kind`, and `graphql.operation.step.plan.id`. A step that sends an operation or an operation batch to a source schema also carries `graphql.source_schema.name` plus either the `graphql.source_schema.operation.*` or the `graphql.source_schema.batch.*` attributes; event stream, introspection, and node steps carry none of them. |

On a step that sends a single operation, `graphql.source_schema.operation.name` carries the generated operation name described in [Generated subgraph operation names changed](#generated-subgraph-operation-names-changed), which makes it the join key between a gateway trace and the matching subgraph log line. A native operation batch step does not carry it: those spans get `graphql.source_schema.batch.operation_count` instead, so join them on `graphql.source_schema.name` and the span timing. Alias batching needs the same treatment for a different reason: it merges the operations as it writes the request, after the span has recorded the plan's name, so the subgraph logs a `_Batch_<compositionHash>` name that the span does not have.

# Aspire

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

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

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

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

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

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

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

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

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

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

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

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

Pass composition options to `WithGraphQLSchemaComposition`:

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

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

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

# Cleanup

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

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

You can also fully replace `ChilliCream.Nitro.CLI` with [`ChilliCream.Nitro.CommandLine`](../../nitro/cli/installation.md).

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

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

---

### Website Content Docs Fusion Migration Migrate From 16 5 To 16 6 (`website/content/docs/fusion/migration/migrate-from-16-5-to-16-6.md`)

---
title: Migrate Hot Chocolate Fusion from 16.5 to 16.6
description: "Migration guide for Hot Chocolate Fusion v16.5 to v16.6: move the Aspire AppHost to the Nitro composition methods and declare the GraphQL route of every composed source schema."
---

> [!NOTE]
> The breaking changes in this release are in the Aspire integration package `HotChocolate.Fusion.Aspire`. A solution without an Aspire AppHost updates the package versions and is done.

# Update the packages

Update every Hot Chocolate Fusion package to 16.6:

```diff
   <ItemGroup>
-    <PackageReference Include="HotChocolate.Fusion.Aspire" Version="16.5.x" />
+    <PackageReference Include="HotChocolate.Fusion.Aspire" Version="16.6.x" />
   </ItemGroup>
```

# Breaking changes

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

## WithGraphQLSchemaComposition renamed to WithNitroComposition

`WithGraphQLSchemaComposition` is deprecated. The only remaining overload takes composition settings as its first parameter, so a 16.5 call that omits the settings or passes the output file name positionally no longer compiles. Rename the call on the gateway resource to `WithNitroComposition`:

```diff
 builder
     .AddProject<Projects.Gateway>("gateway")
-    .WithGraphQLSchemaComposition()
+    .WithNitroComposition()
     .WithReference(products)
     .WithReference(reviews);
```

The parameters changed as well. In 16.5 the method took the output file name first and a settings struct second. 16.6 has two overloads:

```csharp
public static IResourceBuilder<T> WithNitroComposition<T>(
    this IResourceBuilder<T> builder,
    bool disableValidation = false,
    string outputFileName = "gateway.far")
    where T : IResourceWithEndpoints;

public static IResourceBuilder<T> WithNitroComposition<T>(
    this IResourceBuilder<T> builder,
    GraphQLCompositionSettings settings,
    string outputFileName = "gateway.far")
    where T : IResourceWithEndpoints;
```

A positional output file name becomes a named argument:

```diff
-    .WithGraphQLSchemaComposition("composed.far")
+    .WithNitroComposition(outputFileName: "composed.far")
```

A call that passes composition settings puts the settings first:

```diff
-    .WithGraphQLSchemaComposition(
-        settings: new GraphQLCompositionSettings
-        {
-            EnableGlobalObjectIdentification = true
-        })
+    .WithNitroComposition(
+        new GraphQLCompositionSettings
+        {
+            EnableGlobalObjectIdentification = true
+        })
```

Composition settings normally come from Nitro. Settings passed to the `GraphQLCompositionSettings` overload override them locally, so prefer the `disableValidation` overload unless you need a local override.

# Behavioral breaking changes

## Source schemas must declare their GraphQL route

Composition now requires every composed source schema to declare the path of its GraphQL endpoint. `WithGraphQLSchemaEndpoint` and `WithGraphQLSchemaFile` do not declare it. Both are marked `[Obsolete]`, and a resource registered through them still compiles but fails composition with an error like:

```text
The source schema Products of the resource products does not declare the path of its GraphQL endpoint. Call WithGraphQLHttpEndpoint on the resource.
```

Replace both methods with `WithGraphQLHttpEndpoint`:

```diff
 var products = builder
     .AddProject<Projects.Products>("products")
-    .WithGraphQLSchemaEndpoint();
+    .WithGraphQLHttpEndpoint();
```

`WithGraphQLHttpEndpoint` declares the GraphQL route of the resource in addition to the schema download path:

```csharp
public static IResourceBuilder<T> WithGraphQLHttpEndpoint<T>(
    this IResourceBuilder<T> builder,
    string path = "/graphql",
    string? schemaPath = "/graphql/schema.graphql",
    string endpointName = "http",
    string? sourceSchemaName = null)
    where T : IResourceWithEndpoints;
```

| `WithGraphQLSchemaEndpoint` (16.5) | `WithGraphQLHttpEndpoint` (16.6)                    |
| ---------------------------------- | --------------------------------------------------- |
| not declared                       | `path`, the GraphQL route, defaults to `/graphql`   |
| `path`, the schema download path   | `schemaPath`, defaults to `/graphql/schema.graphql` |
| `endpointName`                     | `endpointName`                                      |
| `sourceSchemaName`                 | `sourceSchemaName`                                  |

A custom schema download path moves to `schemaPath`:

```diff
 var products = builder
     .AddProject<Projects.Products>("products")
-    .WithGraphQLSchemaEndpoint(path: "/schema.graphql");
+    .WithGraphQLHttpEndpoint(path: "/graphql", schemaPath: "/schema.graphql");
```

An Apollo Federation source schema serves its schema through the GraphQL endpoint at `path`, so `schemaPath` is ignored for it.

File-based source schemas are being retired. Replace `WithGraphQLSchemaFile` with `WithGraphQLHttpEndpoint`, which downloads the schema from the running resource instead of reading it from the project directory:

```diff
 var products = builder
     .AddProject<Projects.Products>("products")
-    .WithGraphQLSchemaFile("schema.graphqls");
+    .WithGraphQLHttpEndpoint();
```

# Noteworthy changes

## AddGraphQLOrchestrator deprecated in favor of AddNitroComposition

`AddGraphQLOrchestrator` is marked `[Obsolete]` and forwards to `AddNitroComposition`. Rename the call:

```diff
-builder.AddGraphQLOrchestrator();
+builder.AddNitroComposition();
```

`AddNitroComposition` optionally takes a Nitro stage. With a stage, the AppHost composes the local source schemas on top of the fusion configuration that Nitro serves for that stage, and `WithNitroApiId` selects the API a gateway composes against. See [Local Development](../local-development.md) for the full workflow.

```csharp
builder.AddNitroComposition("dev");
```

## Nitro schema validation during composition

When the AppHost composes against a Nitro stage and the gateway selects an API with `WithNitroApiId`, composition validates the composed schema through Nitro. `#!csharp WithNitroComposition(disableValidation: true)` turns the validation off.

## Polyglot AppHost support

The Aspire integration now exports its extension methods to polyglot AppHosts. In a TypeScript AppHost the methods surface through the generated Aspire SDK with camel-cased names and options objects.

# Upgrading from a 16.6 preview

The 16.6 preview builds (`16.6.0-p.x`) shipped intermediate APIs that changed before the release. Skip this section when you upgrade from 16.5.

## AddNitro renamed to AddNitroComposition

The three `AddNitro` overloads collapsed into a single method:

```csharp
public static IDistributedApplicationBuilder AddNitroComposition(
    this IDistributedApplicationBuilder builder,
    string? stage = null,
    Uri? portalUrl = null,
    NitroSeedUpdateOptions? seedUpdates = null);
```

The seed update configure callback became an options object, and the `NitroSeedUpdateOptions` properties are init-only:

```diff
-builder.AddNitro(
-    "dev",
-    portalUrl,
-    options =>
-    {
-        options.AutoUpdate = false;
-    });
+builder.AddNitroComposition(
+    "dev",
+    portalUrl,
+    new NitroSeedUpdateOptions { AutoUpdate = false });
```

Passing `portalUrl` or `seedUpdates` without a stage throws an `ArgumentException`.

## WithNitroApiId requires a resource with endpoints

The generic constraint of `WithNitroApiId` changed from `IResource` to `IResourceWithEndpoints`. A call on a resource builder typed to a resource without endpoints no longer compiles. Project and container resources implement `IResourceWithEndpoints`, so a typical AppHost compiles unchanged.

---

### Website Content Docs Fusion Migration Migrate From 16 6 To 16 7 (`website/content/docs/fusion/migration/migrate-from-16-6-to-16-7.md`)

---
title: Migrate Hot Chocolate Fusion from 16.6 to 16.7
description: "Migration guide for Hot Chocolate Fusion v16.6 to v16.7: account for default cost enforcement, implement the new WebSocket connection initialization diagnostic event, replace raw condition masks with ConditionFlags, configure wide operation limits, review the gateway's refusal of mutations over GET and of incremental delivery without a matching Accept header, review the multipart and request body size limits, and override cost limits per request with FusionRequestCostOptions."
---

Update every Hot Chocolate Fusion package in the application to version 16.7 before applying these changes.

# Breaking changes

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

## IServerDiagnosticEvents gained a WebSocket connection initialization event

`IServerDiagnosticEvents` has a new `WebSocketConnectionInitialized` member. The gateway raises it once per WebSocket session, for both the `graphql-transport-ws` and the legacy `graphql-ws` protocol, after the client's connection initialization message has been accepted.

Listeners that derive from `ServerDiagnosticEventListener` need no change, because the base class provides a virtual no-op. Types that implement `IServerDiagnosticEvents` directly have to implement the new member:

```csharp
public void WebSocketConnectionInitialized(
    ISocketSession session,
    IOperationMessagePayload connectionInitMessage)
{
}
```

The payload of `connectionInitMessage` is only valid for the duration of the call. Read out any value that is needed later inside the callback, for example onto `ISocketConnection.Features`.

## Custom request pipelines must add the cost stages

All three predefined Fusion pipelines now coerce variables, check cost, and only then look up or plan the operation. Update custom pipelines to use the same relative order:

```diff
 builder
     // ... document cache and parser ...
     .UseDocumentValidation()
+    .UseOperationVariableCoercion()
+    .UseCostAnalysis()
     .UseOperationPlanCache()
-    .UseOperationPlan()
-    .UseSkipWarmupExecution()
-    .UseOperationVariableCoercion()
+    .UseOperationPlan()
+    .UseSkipWarmupExecution()
     .UseConcurrencyGate()
     .UseOperationExecution();
```

Document normalization, i.e. inlining fragments into the selected operation, is a lazy service, not a pipeline stage; `OperationVariableCoercion` asks for it on every request and `CostAnalysis` asks for it on a cost-plan cache miss, so nothing needs to be added for it. `OperationVariableCoercion` and `CostAnalysis` now both run before `OperationPlanCache`. Coercion errors therefore precede cost and planning errors, and a cost rejection precedes the operation-plan cache lookup: a rejected request never creates an operation-plan cache entry or an in-flight planning entry. `SkipWarmupExecution` is the only stage that checks whether a request is a warmup request; `OperationVariableCoercion` and `CostAnalysis` coerce and analyze a warmup request exactly like any other request before that stage stops it from executing. `GraphQL-Cost: validate` requests coerce variables exactly like `execute` and `report`, and fail with the ordinary coercion error when required variables are missing.

## Fusion diagnostic event interface expanded

Direct implementations of `IFusionExecutionDiagnosticEvents` or `IFusionExecutionDiagnosticEventListener` must implement these members:

```diff
 public interface IFusionExecutionDiagnosticEvents
 {
+    IDisposable AnalyzeOperationCost(RequestContext context);
+    void OperationCost(
+        RequestContext context,
+        double fieldCost,
+        double typeCost);
 }
```

`AnalyzeOperationCost` scopes cost analysis. `OperationCost` reports each evaluated field-cost and type-cost pair inside that scope.

`FusionExecutionDiagnosticEventListener` supplies implementations for both members, so subclasses do not require changes. `FusionActivityScopes.AnalyzeComplexity` now enables the cost-analysis activity span. It is included in `FusionActivityScopes.All`, but not in `FusionActivityScopes.Default`.

## Cost enforcement is enabled by default

Fusion now enforces a maximum field cost of `1,000` and a maximum type cost of `10,000` in every hosting environment. A request that exceeds either limit returns error code `HC0047` before operation planning.

Passing `disableDefaultSecurity: true` disables cost enforcement as part of disabling the gateway's default security. Cost analysis and `GraphQL-Cost` reporting remain available:

```diff
-services.AddGraphQLGatewayServer();
+services.AddGraphQLGatewayServer(disableDefaultSecurity: true);
```

The assumed size for a list field that carries no applicable `@listSize` information defaults to unbounded (`Infinity`). With default enforcement enabled, an unannotated, non-paginated composite list is rejected with `HC0047` and a `typeCost` of `"Infinity"`. Annotate the source field with `@listSize(assumedSize:)` so composition carries the bound into the composite directive, or set a finite default on the composer's `SourceSchemaMergerOptions.DefaultListSize`. When set, composition writes it onto the execution schema with a schema-level `@fusion__cost_options(defaultListSize:)` directive, and the gateway reads it from there:

```diff
 var options = new SchemaComposerOptions
 {
     Merger =
     {
+        DefaultListSize = 100
     }
 };
```

See [Composition](../composition.md#default-list-size) for details.

## The gateway refuses operations the request does not allow

The gateway now applies the same request checks as a Hot Chocolate server before it executes an operation:

- A mutation sent over HTTP GET has a `405 Method Not Allowed` status code with `Allow: POST`. Previously the gateway executed it. `AllowedGetOperations` controls which operation kinds GET accepts.
- A subscription, or an operation that uses `@defer` or `@stream`, whose `Accept` header names no media type that supports incremental delivery has a `406 Not Acceptable` status code. Previously the gateway executed the operation.
- When `EnableQueryRequests` is `true`, a mutation or subscription sent over HTTP QUERY has a `422 Unprocessable Content` status code.

Send mutations over POST, or set `AllowedGetOperations` to `AllowedGetOperations.QueryAndMutation` to keep accepting them over GET. Send an `Accept` header that includes `multipart/mixed` or `text/event-stream` with operations that use incremental delivery.

## Multipart and request body size limits

- A multipart request whose `operations` field exceeds the [maximum request size](../request-limits.md) is rejected with the error code `HC0010`. 16.6 applied only `FormOptions.MultipartBodyLengthLimit` to that field. Raise `maxAllowedRequestSize` if clients send larger operations.
- A multipart section over `FormOptions.MultipartBodyLengthLimit` is rejected with the error code `HC0135`, which replaces `HC0033`. This includes the `operations` field when `MultipartBodyLengthLimit` is smaller than the maximum request size.
- A request body over the web server's limit, or over `FormOptions.BufferBodyLengthLimit` when `FormOptions.BufferBody` is set, is rejected with the error code `HC0136`, which replaces `HC0012` for a JSON body and `HC0033` for a multipart body.

These responses are HTTP 400, or 413 under the `Draft20260903` transport version; under the `Legacy` transport version, an `application/json` response stays HTTP 200.

# Deprecations

## Raw condition masks replaced by ConditionFlags

Fusion can now compile and execute operations with more than 64 distinct `@skip`/`@include` conditions or `@defer` conditions. `MaxAllowedIncludeConditions` limits the combined `@skip` and `@include` conditions, and `MaxAllowedDeferConditions` limits the `@defer` conditions. Both limits default to **1,024**. An operation that exceeds either limit produces a GraphQL request error during operation compilation.

Configure the limits through `FusionRequestOptions` on the gateway:

```csharp
builder.Services
    .AddGraphQLGatewayServer()
    .ModifyRequestOptions(options =>
    {
        options.MaxAllowedIncludeConditions = 2_048;
        options.MaxAllowedDeferConditions = 2_048;
    });
```

`ConditionFlags` contains the first 64 evaluated conditions and any remaining conditions. Pass the condition carriers from `OperationPlanContext` to the Fusion `Selection` overloads:

```diff
- bool included = selection.IsIncluded(context.IncludeFlags);
- bool deferred = selection.IsDeferred(context.DeferFlags);
+ bool included = selection.IsIncluded(context.IncludeConditionFlags);
+ bool deferred = selection.IsDeferred(context.DeferConditionFlags);
```

Replace every deprecated Fusion `Selection` overload as follows:

| Deprecated 16.6 member                                   | 16.7 replacement                                                  |
| -------------------------------------------------------- | ----------------------------------------------------------------- |
| `Selection.IsIncluded(ulong)`                            | `Selection.IsIncluded(ConditionFlags)`                            |
| `Selection.IsDeferred(ulong)`                            | `Selection.IsDeferred(ConditionFlags)`                            |
| `Selection.GetActiveDeliveryGroups(ulong)`               | `Selection.GetActiveDeliveryGroups(ConditionFlags)`               |
| `Selection.HasActiveDeliveryGroup(ulong, DeliveryGroup)` | `Selection.HasActiveDeliveryGroup(ConditionFlags, DeliveryGroup)` |

The deprecated raw overloads continue to work for operations with at most 64 conditions. When an operation has more than 64 conditions of the corresponding kind, the deprecated raw inclusion overloads throw `InvalidOperationException` for every conditional selection and the deprecated raw defer overloads throw for every deferrable selection, including selections whose own conditions are all among the first 64; raw inclusion evaluation does not throw for an unconditional selection, and raw defer evaluation does not throw for a non-deferrable selection. Releases before 16.7 rejected operations with more than 64 conditions during compilation.

---

### Website Content Docs Fusion Migration Migrating From Schema Stitching (`website/content/docs/fusion/migration/migrating-from-schema-stitching.md`)

---
title: "Migrating from Schema Stitching"
description: "Migrate from Hot Chocolate Schema Stitching to Fusion: replace @delegate resolvers with [Lookup] fields and build-time composition using `nitro fusion compose`."
---

# Migrating from Schema Stitching to Fusion

If your team uses HotChocolate Schema Stitching to combine multiple GraphQL services into a single API, this guide walks you through migrating to HotChocolate Fusion. Fusion is the evolution of the same idea -- distributed GraphQL under a single endpoint -- but with a fundamentally different architecture that catches schema conflicts at build time, eliminates manual resolver delegation, and aligns with the open [GraphQL Composite Schemas specification](https://graphql.github.io/composite-schemas-spec/).

This guide is self-contained. You can complete the migration by following the steps here. Links to other Fusion documentation pages are provided for deeper dives, but they are not prerequisites.

## How the Concepts Map

If you have worked with Schema Stitching, you already understand the core idea: multiple GraphQL services combined into one schema. Fusion keeps that idea but changes how it works under the hood. Here is how the concepts translate:

| Schema Stitching                                               | Fusion                                             | What Changed                                                                                                                               |
| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Remote schemas (`AddRemoteSchema()`)                           | Subgraphs (source schemas)                         | Each remote schema becomes its own standalone ASP.NET Core project with HotChocolate. No stitching middleware needed.                      |
| Stitching gateway (`AddGraphQLServer()` + `AddRemoteSchema()`) | Fusion gateway (`AddGraphQLGateway()`)             | The gateway is stateless. No custom resolvers, no delegation logic, no type extensions in the gateway project.                             |
| Schema extensions (`.graphql` extension files)                 | Entity stubs with `[ObjectType<T>]`                | Instead of writing SDL extension files with `@delegate` directives, you define C# types in the subgraph that extends the entity.           |
| Delegating resolvers (`@delegate` directive)                   | Lookups (`[Lookup]` attribute)                     | The gateway handles cross-subgraph resolution automatically. You declare a lookup field in each subgraph; composition wires them together. |
| `@delegate(path: "...")` field references                      | `[Require]` attribute                              | When a field needs data from another subgraph, you declare the dependency as a method parameter with `[Require]`.                          |
| Auto-stitching / runtime schema merging                        | Build-time composition (`nitro fusion compose`)    | Schemas are merged offline by the Nitro CLI, producing a static configuration file. Conflicts are caught before deployment.                |
| `PublishSchemaDefinition()` + Redis                            | `schema export` + `nitro fusion upload`            | Schema distribution uses the Nitro CLI or Aspire instead of Redis pub/sub.                                                                 |
| `RenameType()` / `RenameField()` / `IgnoreType()`              | Composition rules + `[Internal]` / `@inaccessible` | Type conflicts are resolved by composition rules. Fields you want to hide use `[Internal]` on lookups or `@inaccessible` on types.         |
| `SchemaDefinition` / `SchemaExtension`                         | `schema.graphqls` + `schema-settings.json`         | Exported automatically by the subgraph on startup. You do not write these by hand.                                                         |

## What Changes Architecturally

The migration is not just swapping one API for another. Fusion changes the architecture in ways that simplify your system but require rethinking where code lives.

### The Gateway Becomes Stateless

In Schema Stitching, the gateway is where you configure everything: register remote schemas, define type extensions, write delegating resolvers, rename types to avoid conflicts, and set up context data propagation. The gateway is the brain of the system.

In Fusion, the gateway has **no custom code**. It loads a pre-composed configuration file (a `.far` archive) and uses it to route queries to subgraphs. All business logic, type definitions, and resolver code live in the subgraphs. If you have custom delegating resolvers or type extensions in your stitching gateway, you will need to move that logic into the appropriate subgraph.

### All Business Logic Moves to Subgraphs

In stitching, it was common to define type extensions and delegating resolvers in the gateway using `.graphql` extension files:

```graphql filename="Gateway/Stitching.graphql"
extend type Product {
  inStock: Boolean
    @delegate(
      schema: "inventory"
      path: "inventoryInfo(upc: $fields:upc).isInStock"
    )
  shippingEstimate: Int
    @delegate(
      schema: "inventory"
      path: "shippingEstimate(price: $fields:price, weight: $fields:weight)"
    )
}
```

In Fusion, this logic moves to the subgraph that owns the extended field. The Inventory subgraph itself declares how it extends the `Product` type using C# code. The gateway never sees or manages these relationships.

### Composition Replaces Runtime Schema Merging

Schema Stitching merges schemas at runtime when the gateway starts. If two remote schemas define conflicting types, you discover the problem when the gateway fails to start.

Fusion merges schemas at build time using the Nitro CLI (`nitro fusion compose`). You run composition as part of your build or CI pipeline. If schemas conflict, composition fails with a clear error message -- before you deploy anything. This means you catch issues like missing `[Shareable]` annotations, incompatible field types, or enum value mismatches during development, not at 3 AM in production.

### Transport: HTTP Between Gateway and Subgraphs

In some stitching setups, remote schemas could run in-process or communicate over custom protocols. In Fusion, the gateway communicates with subgraphs over HTTP. Each subgraph is a standalone ASP.NET Core application with its own HTTP endpoint. The gateway sends GraphQL requests to each subgraph's `/graphql` endpoint and aggregates the responses.

## Step-by-Step Migration

This section walks through converting a typical Schema Stitching setup to Fusion. The examples use a common pattern: a Products service and an Inventory service, where the gateway extends the `Product` type with inventory data.

### Step 1: Convert Remote Schemas to Standalone Subgraphs

In stitching, your remote schemas are HotChocolate servers that may or may not publish their schema definitions. In Fusion, each remote schema becomes a standalone subgraph -- a regular HotChocolate server with a few additional attributes.

**Before (Stitching remote schema):**

```csharp filename="Products/Program.cs"
var builder = WebApplication.CreateBuilder(args);

builder
    .AddGraphQL()
    .AddQueryType<ProductQueries>();

var app = builder.Build();
app.MapGraphQL();
app.Run();
```

```csharp filename="Products/ProductQueries.cs"
public class ProductQueries
{
    public Product GetProductByUpc(int upc)
        => ProductRepository.GetByUpc(upc);

    public IEnumerable<Product> GetProducts()
        => ProductRepository.GetAll();
}
```

**After (Fusion subgraph):**

```csharp filename="Products/Program.cs"
var builder = WebApplication.CreateBuilder(args);

builder
    .AddGraphQL("products-api")
    .AddProductTypes()
    .AddGlobalObjectIdentification()
    .AddMutationConventions()
    .ExportSchemaOnStartup();

var app = builder.Build();
app.MapGraphQL();
app.RunWithGraphQLCommands(args);
```

```csharp filename="Products/Types/ProductQueries.cs"
[QueryType]
public static partial class ProductQueries
{
    [Lookup]
    public static async Task<Product?> GetProductByUpc(
        int upc,
        IProductByUpcDataLoader productByUpc,
        CancellationToken cancellationToken)
        => await productByUpc.LoadAsync(upc, cancellationToken);

    [UsePaging]
    public static async Task<Connection<Product>> GetProducts(
        PagingArguments arguments,
        ProductContext context,
        CancellationToken cancellationToken)
        => await context.Products
            .OrderBy(p => p.Name)
            .ThenBy(p => p.Upc)
            .ToPageAsync(arguments, cancellationToken)
            .ToConnectionAsync();
}
```

Key changes:

- **`#!csharp builder.AddGraphQL("products-api")`** replaces `builder.AddGraphQL()`. The string argument is the subgraph name used during composition.
- **`.AddProductTypes()`** is generated by the `HotChocolate.Types.Analyzers` package. It registers all types marked with `[QueryType]`, `[ObjectType<T>]`, etc.
- **`.ExportSchemaOnStartup()`** exports the subgraph's schema as a `.graphqls` file when the server starts (used for composition).
- **`app.RunWithGraphQLCommands(args)`** enables CLI commands like `#!shell dotnet run -- schema export`.
- **`[Lookup]`** on `GetProductByUpc` tells Fusion that this field can be used to resolve `Product` entities from other subgraphs. This is the Fusion equivalent of what stitching resolvers delegated to.
- **`[QueryType]`** and `static partial class` use HotChocolate's annotation-based type system.

You also need a `schema-settings.json` file in the subgraph project root:

```json
{
  "name": "products-api",
  "transports": {
    "http": {
      "clientName": "fusion",
      "url": "{{API_URL}}"
    }
  },
  "environments": {
    "development": {
      "API_URL": "http://localhost:5100/graphql"
    },
    "production": {
      "API_URL": "https://products.example.com/graphql"
    }
  }
}
```

This file tells the composition engine the subgraph's name and how the gateway should reach it. The `{{API_URL}}` placeholder is resolved from the active environment.

Add these packages to the subgraph `.csproj`:

```xml
<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>
```

Repeat this for every remote schema in your stitching setup. Each one becomes its own ASP.NET Core project.

### Step 2: Replace Schema Extensions with Entity Stubs

In stitching, when you want to add fields from one service to a type owned by another service, you write SDL extension files with `@delegate` directives. In Fusion, you create **entity stubs** in the subgraph that contributes the additional fields.

**Before (Stitching -- SDL extension file in the gateway or domain service):**

```graphql filename="Stitching.graphql"
extend type Product {
  inStock: Boolean
    @delegate(
      schema: "inventory"
      path: "inventoryInfo(upc: $fields:upc).isInStock"
    )
  shippingEstimate: Int
    @delegate(
      schema: "inventory"
      path: "shippingEstimate(price: $fields:price, weight: $fields:weight)"
    )
}
```

**After (Fusion -- C# code in the Inventory subgraph):**

First, create an entity stub for `Product` in the Inventory subgraph. This is a lightweight C# type that declares "I know `Product` exists, identified by `upc`, and I want to add fields to it":

```csharp filename="Inventory/Types/Product.cs"
[EntityKey("upc")]
public sealed record Product(int Upc)
{
    public bool GetInStock(
        [Parent(requires: nameof(Upc))] Product product,
        InventoryRepository repository)
        => repository.IsInStock(product.Upc);

    public int GetShippingEstimate(
        [Require("""
            {
              weight,
              price
            }
            """)]
        ShippingInput input)
        => ShippingCalculator.Estimate(input.Weight, input.Price);
}
```

```csharp filename="Inventory/Types/ShippingInput.cs"
public sealed class ShippingInput
{
    public int Weight { get; init; }
    public double Price { get; init; }
}
```

Then add an internal lookup so the gateway can resolve `Product` references within this subgraph:

```csharp filename="Inventory/Types/InventoryQueries.cs"
[QueryType]
public static partial class InventoryQueries
{
    [Lookup, Internal]
    public static Product GetProductByUpc(int upc)
        => new(upc);
}
```

Key concepts:

- **Entity stub**: `#!csharp record Product(int Upc)` is not a copy of the Products subgraph's `Product` class. It is a minimal declaration that tells Fusion: "I know `Product` exists, it has a `upc` key, and I want to contribute fields to it." The gateway merges these fields with the full `Product` from the Products subgraph.
- **`#!csharp [EntityKey("upc")]`**: Declares which field identifies this entity. This replaces the `$fields:upc` reference in stitching's `@delegate` directive.
- **`[Require]`**: Declares that `GetShippingEstimate` needs `weight` and `price` from the composed `Product` type. The gateway fetches these fields from the Products subgraph before calling this resolver. This replaces `$fields:price` and `$fields:weight` from the `@delegate` path. The `[Require]` attribute uses a GraphQL-like selection syntax to map fields from the composed type into a C# input object.
- **`[Lookup, Internal]`**: The internal lookup lets the gateway resolve `Product` references within the Inventory subgraph. `[Internal]` means this lookup is not exposed to clients -- it is only used internally by the gateway during query planning. This replaces the implicit entity resolution that stitching handled through `@delegate`.

### Step 3: Replace the Stitching Gateway with a Fusion Gateway

The Fusion gateway is dramatically simpler than a stitching gateway because it has no custom resolver logic.

**Before (Stitching gateway):**

```csharp filename="Gateway/Program.cs"
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddHttpClient("products", c =>
        c.BaseAddress = new Uri("http://localhost:5100/graphql"));
builder.Services
    .AddHttpClient("inventory", c =>
        c.BaseAddress = new Uri("http://localhost:5200/graphql"));

builder
    .AddGraphQL()
    .AddRemoteSchema("products")
    .AddRemoteSchema("inventory")
    .AddTypeExtensionsFromFile("./Stitching.graphql");

var app = builder.Build();
app.MapGraphQL();
app.Run();
```

**After (Fusion gateway):**

```csharp filename="Gateway/Program.cs"
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddHttpClient("fusion")
    .AddHeaderPropagation();

builder.Services.AddHeaderPropagation(c =>
{
    c.Headers.Add("Authorization");
});

builder
    .AddGraphQLGateway()
    .AddFileSystemConfiguration("./gateway.far");

var app = builder.Build();
app.UseHeaderPropagation();
app.MapGraphQL();
app.Run();
```

Key changes:

- **`AddGraphQLGateway()`** replaces `AddGraphQLServer()` + `AddRemoteSchema()`. The gateway does not know about individual subgraphs -- it reads the composed configuration.
- **`#!csharp .AddFileSystemConfiguration("./gateway.far")`** loads the composed Fusion archive. This file is produced by `nitro fusion compose` (see Step 4). Alternatively, use `.AddNitro()` to download the configuration from ChilliCream's cloud platform.
- **No type extensions, no `@delegate`, no remote schema registration.** The gateway is pure routing infrastructure.
- **Header propagation** is configured through standard ASP.NET Core middleware. The named HTTP client `"fusion"` is what the gateway uses to call subgraphs. You add `AddHeaderPropagation()` on it to forward headers like `Authorization` to subgraphs.

Add these packages to the gateway `.csproj`:

```xml
<ItemGroup>
    <PackageReference Include="HotChocolate.Fusion.AspNetCore" />
    <PackageReference Include="Microsoft.AspNetCore.HeaderPropagation" />
</ItemGroup>
```

Notice what is gone: no `HotChocolate.Stitching` package, no per-service HTTP clients with hardcoded URLs, no extension files. The gateway project is minimal.

### Step 4: Compose Your Schemas

In stitching, schema merging happens at runtime when the gateway starts. In Fusion, you compose schemas offline using the Nitro CLI.

First, export each subgraph's schema:

```shell
# In the Products subgraph project directory
dotnet run -- schema export

# In the Inventory subgraph project directory
dotnet run -- schema export
```

Each command produces a `schema.graphqls` file alongside the `schema-settings.json` in the subgraph project.

Then compose all subgraphs into a Fusion archive:

```shell
nitro fusion compose \
  --source-schema-file ./src/Products/schema.graphqls \
  --source-schema-file ./src/Inventory/schema.graphqls \
  --archive ./src/Gateway/gateway.far \
  --environment development
```

If composition succeeds, you get a `gateway.far` file that the gateway loads at startup. If it fails, you get error messages telling you exactly which types or fields conflict and how to fix them.

During development, you can use watch mode to recompose automatically when schema files change:

```shell
nitro fusion compose \
  --source-schema-file ./src/Products/schema.graphqls \
  --source-schema-file ./src/Inventory/schema.graphqls \
  --archive ./src/Gateway/gateway.far \
  --environment development \
  --watch
```

Install the Nitro CLI if you have not already:

```shell
dotnet tool install -g ChilliCream.Nitro.CLI
```

### Step 5: Move Gateway-Side Logic to Subgraphs

This is the step that requires the most thought. In stitching, it was common to put logic in the gateway:

- **Type renames** (`RenameType()`, `RenameField()`): In Fusion, naming conflicts are resolved by composition rules. If two subgraphs define the same type and their definitions are compatible, composition merges them automatically. If they conflict, composition fails with an error. To resolve conflicts, you adjust the subgraph schemas rather than renaming at the gateway level.

- **Field ignoring** (`IgnoreField()`, `IgnoreType()`): In Fusion, use `[Internal]` on lookups to hide them from the composed schema, or `@inaccessible` on types/fields you want to exclude from the composite schema.

- **Context data propagation** (`$contextData`, `$scopedContextData`): In Fusion, cross-subgraph data dependencies are declared with `[Require]`. The gateway resolves the required fields automatically -- you do not pass context data manually.

- **Custom middleware** (`UseField`, `UseRequest`): Any per-field middleware or request interceptors that lived in the stitching gateway must move to the appropriate subgraph. Each subgraph is a full HotChocolate server and supports the same middleware pipeline.

- **Redis-based schema publishing** (`PublishToRedis()`): Replace with the Nitro CLI workflow. Subgraphs export their schemas on startup (via `ExportSchemaOnStartup()`), and the Nitro CLI handles composition and distribution.

### Step 6: Verify and Test

Once all subgraphs are converted and the gateway is set up:

1. **Start all subgraphs** -- each runs as its own ASP.NET Core application.
2. **Export schemas** from each subgraph (`#!shell dotnet run -- schema export`).
3. **Run composition** (`nitro fusion compose`) and fix any errors.
4. **Start the gateway** with the composed `.far` file.
5. **Run your existing queries** against the gateway endpoint and verify the results match what the stitching gateway returned.

Pay special attention to:

- Fields that were added via `@delegate` -- verify they resolve correctly through the new entity stubs and lookups.
- Queries that relied on type renames or field renames -- the composed schema may have different names.
- Context data that was propagated through `$contextData` -- verify that `[Require]` provides the equivalent data to resolvers.

## What Changes at Runtime

Beyond the code changes, Fusion behaves differently at runtime compared to Schema Stitching.

### Query Planning vs. Delegation

In stitching, the gateway delegates individual field resolutions to remote schemas using the `@delegate` directive. Each delegated field is a separate remote call, and the stitching engine handles the orchestration.

In Fusion, the gateway creates a **query plan** at request time. It analyzes the full query, determines which subgraphs own which fields, groups fetches to minimize round trips, and executes them in an optimized order. This means Fusion can batch entity lookups (fetching multiple products in a single call) and parallelize independent fetches -- something that stitching's per-field delegation could not do efficiently.

### Error Handling

In stitching, errors from remote schemas are propagated through the delegation chain. The error format and propagation behavior depended on the delegation configuration.

In Fusion, when a subgraph returns an error, the gateway includes it in the response's `errors` array with path information pointing to the field that failed. If a subgraph is unreachable, the gateway returns `null` for fields from that subgraph (if the field is nullable) or propagates the error upward. You can configure HTTP resilience on the gateway's named HTTP client using `Microsoft.Extensions.Http.Resilience`:

```csharp
builder.Services
    .AddHttpClient("fusion")
    .AddStandardResilienceHandler();
```

### Transport

Stitching communicated with remote schemas via HTTP, but some setups used in-process schema registration or Redis for schema discovery. Fusion uses HTTP for all gateway-to-subgraph communication. Each subgraph must be reachable at the URL specified in its `schema-settings.json`. The gateway uses a single named HTTP client (`"fusion"` by convention) for all subgraph calls.

## What Gets Simpler

After migrating, several things that required manual work in stitching become automatic:

- **No manual schema delegation.** You do not write `@delegate` directives or manage delegation paths. Composition figures out how to resolve cross-subgraph fields from the lookups and entity stubs you declare.

- **Entity resolution is a simple lookup field.** Instead of constructing complex `@delegate(path: "...")` expressions with `$fields` references, you write a C# method with `[Lookup]` that takes an ID and returns an entity. The gateway calls it when needed.

- **Composition catches conflicts at build time.** In stitching, type conflicts were discovered when the gateway started (or worse, when a specific query triggered the conflict at runtime). Fusion's offline composition validates everything upfront.

- **Independent subgraph deployment.** Each subgraph is a standalone ASP.NET Core application. You can deploy, scale, and update them independently. Adding a new subgraph does not require changes to existing subgraphs or the gateway code.

- **No gateway code changes when adding subgraphs.** In stitching, adding a remote schema meant updating the gateway's `Program.cs` (adding `AddRemoteSchema()`, HTTP clients, and extension files). In Fusion, you add the new subgraph's schema to the composition step and redeploy the gateway with the updated `.far` file. No gateway code changes.

- **No Redis dependency for schema distribution.** If you used `PublishToRedis()` for schema federation, Fusion replaces that with the Nitro CLI or Aspire orchestration.

## Common Pitfalls

### Forgetting That the Gateway is Stateless

The most common mistake is trying to put logic in the Fusion gateway. If you have type extensions, custom resolvers, middleware, or `UseField` hooks in your stitching gateway, all of that must move to a subgraph. The Fusion gateway project should contain only the gateway setup (`AddGraphQLGateway()`), HTTP client configuration, and middleware pipeline (auth, CORS, header propagation).

### Confusing Entity Stubs with Data Duplication

When you create `#!csharp record Product(int Upc)` in the Inventory subgraph, it looks like you are duplicating the `Product` type. You are not. This entity stub is a declaration: "I know `Product` exists, it has a `upc` key, and I want to add fields to it." The stub does not contain the full `Product` definition -- the Products subgraph owns that. Composition merges the stub's contributed fields with the full type.

### Missing Internal Lookups

If a subgraph extends an entity from another subgraph (like the Inventory subgraph extending `Product`), it needs an internal lookup so the gateway can resolve `Product` references within that subgraph:

```csharp
[QueryType]
public static partial class InventoryQueries
{
    [Lookup, Internal]
    public static Product GetProductByUpc(int upc)
        => new(upc);
}
```

Without this lookup, the gateway cannot route queries that involve the Inventory subgraph's contributed fields on `Product`. Composition will report an error if an entity is used in a subgraph without a corresponding lookup.

### schema-settings.json Misconfiguration

Every subgraph needs a `schema-settings.json` file with the correct:

- **`name`**: Must be unique across all subgraphs. This is the identifier used during composition.
- **`transports.http.url`**: Must point to the subgraph's actual GraphQL endpoint. Use `{{API_URL}}` with environment-specific values.
- **`transports.http.clientName`**: Must match the named HTTP client registered in the gateway (typically `"fusion"`).

If the URL is wrong, the gateway will fail to reach the subgraph at runtime. If the name is wrong or duplicated, composition will produce incorrect results.

### Fields That Need Sharing

In stitching, if two remote schemas defined the same field, the gateway's auto-resolution would prefix one with the schema name. In Fusion, if two subgraphs define the same non-key field on the same type, composition fails unless the field is marked `[Shareable]` in both subgraphs.

For example, if both the Accounts subgraph and the Reviews subgraph define `User.name`:

```csharp
// In BOTH subgraphs:
[ObjectType<User>]
public static partial class UserNode
{
    [Shareable]
    public static string GetName([Parent] User user)
        => user.Name!;
}
```

`[Shareable]` tells Fusion: "this field is intentionally defined in multiple subgraphs and they all return the same value." The gateway can resolve it from whichever subgraph is most convenient for a given query. For more on field ownership rules, see [Entities and Lookups](../entities-and-lookups.md).

## Migrating CI/CD

If your stitching setup uses Redis-based schema federation with CI/CD automation, the pipeline changes significantly.

**Before (Stitching with Redis federation):**

1. Build and deploy each domain service.
2. Each service publishes its schema to Redis on startup via `PublishToRedis()`.
3. The gateway subscribes to Redis and picks up schema changes at runtime.

**After (Fusion with Nitro CLI):**

1. Build each subgraph and export its schema: `#!shell dotnet run -- schema export`.
2. Upload the schema to ChilliCream Nitro: `#!shell nitro fusion upload --source-schema-file schema.graphqls --tag v1.0.0 --api-id <id> --api-key <key>`.
3. Deploy the subgraph container.
4. Publish the composed configuration: `#!shell nitro fusion publish --source-schema products-api --tag v1.0.0 --stage production --api-id <id> --api-key <key>`.
5. The gateway downloads the updated configuration from Nitro automatically (via `.AddNitro()` in the gateway setup).

Alternatively, for a simpler setup without Nitro cloud:

1. Export schemas from all subgraphs.
2. Run `nitro fusion compose` locally or in CI to produce a `.far` file.
3. Deploy the `.far` file alongside the gateway.
4. The gateway loads the `.far` file on startup via `#!csharp .AddFileSystemConfiguration("./gateway.far")`.

For a full CI/CD pipeline reference, see [Deployment and CI/CD](../deployment-and-ci-cd.md).

## Summary

Migrating from Schema Stitching to Fusion is a structural change, not just an API swap. The key shifts are:

1. **Gateway becomes stateless** -- move all type extensions, delegating resolvers, and custom middleware into subgraphs.
2. **Entity stubs replace schema extensions** -- instead of SDL files with `@delegate`, write C# types with `[Lookup]` and `[Require]` in the subgraph.
3. **Build-time composition replaces runtime merging** -- use `nitro fusion compose` to validate and merge schemas before deployment.
4. **Each subgraph is a standalone server** -- no stitching middleware, no Redis dependencies, just a HotChocolate server with a few extra attributes.

The effort involved depends on the complexity of your stitching setup. If your gateway is mostly auto-stitching with minimal custom delegation, the migration is straightforward. If your gateway has extensive custom resolvers, `$contextData` propagation, and type manipulation, expect to spend more time restructuring that logic into subgraphs. The result is a cleaner architecture where each subgraph owns its complete behavior and the gateway is pure infrastructure.

---

### Website Content Docs Hotchocolate Adapters Mcp (`website/content/docs/hotchocolate/adapters/mcp.md`)

---
title: "MCP"
description: "Turn a Hot Chocolate GraphQL server into an MCP server with `AddMcp()` and `MapGraphQLMcp()`, exposing tools and prompts to AI assistants over Streamable HTTP."
---

The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard that lets AI assistants connect to external systems through a uniform tool and prompt interface. The `HotChocolate.Adapters.Mcp` package turns a Hot Chocolate GraphQL server into an MCP server. You supply tool and prompt definitions through an `IMcpStorage`, and the adapter handles execution, transport, and live updates. Definitions can come from Nitro (file-based authoring with a publish workflow) or from a custom `IMcpStorage` you build (programmatic, database-backed, or any source you choose).

You wire MCP onto an existing Hot Chocolate server with two calls: `AddMcp()` during service registration and `MapGraphQLMcp()` during endpoint mapping. The adapter exposes the MCP server over Streamable HTTP at `/graphql/mcp` by default, so any MCP client (Claude Desktop, an editor extension, an agent runtime) can connect directly to your server.

This page covers wiring and configuration. For authoring tools and prompts, see the [Nitro MCP](../../nitro/adapters/mcp.md) section.

# Prerequisites

You need an existing Hot Chocolate GraphQL server. If you do not have one yet, follow the [Get started with Hot Chocolate](../get-started-with-graphql-in-net-core.md) tutorial first.

Add the adapter package to the server project:

```shell
dotnet add package HotChocolate.Adapters.Mcp
```

# Enabling MCP on the Server

Two adapter calls turn a Hot Chocolate server into an MCP server. `AddMcp()` registers the MCP server, schema services, and a startup warmup that loads tool and prompt definitions from storage. `MapGraphQLMcp()` exposes the MCP transport endpoints.

```csharp
builder
    .AddGraphQL()
    .AddMcp();
    // Storage is required: see "Connecting a Tool and Prompt Source" below.

// ...

app.MapGraphQLMcp();
```

The server needs a tool and prompt source. Without one, `MapGraphQLMcp()` throws `InvalidOperationException` during startup. Wire up storage by either [using Nitro](#using-nitro-for-tools-and-prompts) or providing a custom `IMcpStorage`.

# Connecting a Tool and Prompt Source

The adapter does not ship tools or prompts of its own. It asks an `IMcpStorage` implementation for them at startup, and listens for change notifications afterwards. You have two options:

1. **Use Nitro** (recommended for production). Nitro publishes versioned MCP feature collections to the server and supplies an `IMcpStorage` automatically. Skip ahead to [Using Nitro](#using-nitro-for-tools-and-prompts).
2. **Provide your own `IMcpStorage`** for self-hosted scenarios where you manage tool definitions outside Nitro.

To register a custom storage, implement `IMcpStorage` and pass it to `AddMcpStorage()`:

```csharp
builder
    .AddGraphQL()
    .AddMcp()
    .AddMcpStorage<MyMcpStorage>();
```

Three overloads cover the common registration patterns:

| Overload                                             | Use when                                                                  |
| ---------------------------------------------------- | ------------------------------------------------------------------------- |
| `AddMcpStorage(IMcpStorage instance)`                | You already have a singleton instance.                                    |
| `AddMcpStorage<T>()`                                 | You want DI to construct the storage. `T` is activated from app services. |
| `AddMcpStorage(Func<IServiceProvider, IMcpStorage>)` | You need a factory for custom construction or scoping.                    |

`IMcpStorage` returns `OperationToolDefinition` and `PromptDefinition` collections, and exposes `IObservable` streams so the server can apply update and remove events without restarting. Reach for this extension point only when you cannot use Nitro, since implementing it correctly involves change diffing, caching, and reactive subscriptions.

# Configuring the MCP Server

`AddMcp()` accepts two configuration delegates. The first targets `McpServerOptions` (server behavior), the second targets `IMcpServerBuilder` (tool, prompt, and resource registration from the underlying MCP SDK):

```csharp
builder
    .AddGraphQL()
    .AddMcp(
        configureServerOptions: options =>
        {
            options.InitializationTimeout = TimeSpan.FromSeconds(30);
        },
        configureServer: server =>
        {
            // Register additional MCP server features here.
        });
```

Tools registered through `configureServer` appear alongside the GraphQL-derived tools, so you can mix native MCP tools with operation tools in the same server.

# Mapping the MCP Endpoint

`MapGraphQLMcp()` accepts two optional arguments:

```csharp
app.MapGraphQLMcp(pattern: "/graphql/mcp", schemaName: null);
```

- **`pattern`**: the URL prefix for the MCP transport. Defaults to `/graphql/mcp`. Change it when the default conflicts with another route or when you expose multiple schemas from the same host.
- **`schemaName`**: the named schema to expose. The adapter resolves this automatically when the server has a single schema. Pass it explicitly when the host registers multiple named schemas, so each schema gets its own MCP endpoint:

```csharp
app.MapGraphQLMcp("/graphql/public/mcp", schemaName: "Public");
app.MapGraphQLMcp("/graphql/internal/mcp", schemaName: "Internal");
```

The endpoint speaks Streamable HTTP. POST carries JSON-RPC requests and returns either `text/event-stream` for streaming responses or `202 Accepted` for queued ones.

# Using Nitro for Tools and Prompts

Nitro is the easiest way to manage MCP tools and prompts. You author tools and prompts on disk, upload them as a tagged version of a feature collection with the Nitro CLI, and publish that version to a stage. The server loads the collection from the configured stage and picks up new versions automatically. When Nitro is wired up alongside `AddMcp()`, it registers an `IMcpStorage` for you, so you do not call `AddMcpStorage()` yourself.

## Install the Nitro packages

```shell
dotnet add package ChilliCream.Nitro
dotnet add package ChilliCream.Nitro.HotChocolate
```

`ChilliCream.Nitro` is the core package and includes a source generator that emits an `AddDefaults()` extension method based on which integration packages are referenced in the project. With `ChilliCream.Nitro.HotChocolate` referenced, `AddDefaults()` calls `AddHotChocolate()` for you.

## Wire Nitro into the server

Call `AddNitro().AddDefaults()` (or the explicit `AddNitro().AddHotChocolate()`) before configuring the GraphQL server. `AddNitro()` configures the shared connection options (`ApiId`, `ApiKey`, `Stage`) on `NitroServiceOptions`. `ModifyNitroOptions()` on the GraphQL builder configures schema-specific options (MCP, OpenAPI, persisted operations, metrics, and so on):

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddNitro(o =>
    {
        o.ApiId = builder.Configuration["Nitro:ApiId"]!;
        o.ApiKey = builder.Configuration["Nitro:ApiKey"]!;
        o.Stage = builder.Configuration["Nitro:Stage"]!;
    })
    .AddDefaults();

builder
    .AddGraphQL()
    .ModifyNitroOptions(o =>
    {
        // Modify Nitro options here.
    })
    .AddMcp();

var app = builder.Build();

app.MapGraphQL();
app.MapGraphQLMcp();

app.Run();
```

If you prefer environment variables over inline configuration, set `NITRO_API_ID`, `NITRO_STAGE`, and `NITRO_API_KEY`. The Nitro service options bind to these automatically and you can drop the `AddNitro` configuration delegate entirely.

> Order matters: `AddNitro().AddDefaults()` (or `AddHotChocolate()`) must run before the GraphQL builder calls so that the Hot Chocolate pipeline picks up the Nitro contributions during registration.

## What you get

With Nitro and MCP both enabled:

- The server loads the published MCP feature collection for the configured stage on startup.
- Tool and prompt definitions are cached locally so cold starts work without a round trip to Nitro.
- Stage change events flow over the Nitro change feed. When you publish a new version, the server updates its tool and prompt set in place, no restart required.
- If Nitro configuration is incomplete (any of `ApiId`, `ApiKey`, or `Stage` not set), MCP integration is disabled with a warning, the storage returns no definitions, and the host continues to start.
- If configuration is set but the API key is rejected, the storage uses the local cache when one is available and logs the sync failure. Without a usable cache, the exception propagates and host startup fails.

For authoring tools and prompts, publishing feature collection versions, and managing stages, see the [Nitro MCP](../../nitro/adapters/mcp.md) section.

# Troubleshooting

## `InvalidOperationException: Call AddMcp() when configuring the GraphQL server.`

`MapGraphQLMcp()` was called but `AddMcp()` was not registered on the GraphQL server builder. Add `.AddMcp()` to the chain that starts with `AddGraphQL()`.

## MCP endpoint returns 404 Not Found

The route pattern does not match what your client uses. The default is `/graphql/mcp`. If you passed a custom pattern to `MapGraphQLMcp()`, point your client at it.

## `InvalidOperationException: No IMcpStorage is registered for schema '<name>'.`

Two possible causes:

- **No storage source for that schema.** `AddMcp()` was registered, but no `IMcpStorage` is wired up. Either reference `ChilliCream.Nitro.HotChocolate` and call `AddNitro().AddDefaults()` (or `AddNitro().AddHotChocolate()`), or register a custom storage with `AddMcpStorage(...)`.
- **Endpoint mapped to the wrong schema.** `MapGraphQLMcp(pattern, schemaName)` was called with a `schemaName` that does not match any schema registered through `AddGraphQL(name)` plus `AddMcp()`. With multiple named schemas, pass the matching name. With a single unnamed schema, omit `schemaName` and the adapter resolves it automatically.

## Tools and prompts list is empty

Storage is registered but returned no definitions. With Nitro, ensure a published MCP feature collection version exists for the configured stage and that the API key has read access to it. With a custom `IMcpStorage`, verify that the implementation completes its initial fetch before the warmup task times out and that it returns the expected definitions.

## Nitro logs `MCP integration is disabled because Nitro is not properly configured.`

`NitroServiceOptions` is missing one or more of `ApiId`, `ApiKey`, or `Stage`. Set them through the `AddNitro()` configuration delegate or via the `NITRO_API_ID`, `NITRO_API_KEY`, and `NITRO_STAGE` environment variables.

# Next Steps

- Author tools and prompts and publish them with Nitro in the [Nitro MCP](../../nitro/adapters/mcp.md) section.
- Customize how GraphQL errors surface in MCP tool results with [Errors](../resolvers/errors.md).

---

### Website Content Docs Hotchocolate Adapters Openapi (`website/content/docs/hotchocolate/adapters/openapi.md`)

---
title: "OpenAPI Adapter"
description: "Expose REST endpoints from a Hot Chocolate GraphQL schema with the AddOpenApi() adapter: annotate operations with @http and get generated OpenAPI docs."
---

The OpenAPI adapter exposes your Hot Chocolate GraphQL schema as REST endpoints with automatic OpenAPI documentation. You define GraphQL operations annotated with `@http` directives, and the adapter generates HTTP endpoints that accept REST-style requests, execute the underlying GraphQL operation, and return the result as JSON. The generated endpoints appear in your OpenAPI specification alongside any other ASP.NET Core endpoints.

This is useful when you have a GraphQL API and need to provide a REST interface for clients that do not support GraphQL, or when you want to offer both GraphQL and REST access to the same backend.

# Setup

Install the `HotChocolate.Adapters.OpenApi` package:

```shell
dotnet add package HotChocolate.Adapters.OpenApi
```

Register the adapter on your GraphQL server and map the endpoints:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddRouting()
    .AddOpenApi(options => options.AddGraphQLTransformer());

builder
    .AddGraphQL()
    .AddQueryType<Query>()
    .AddMutationType<Mutation>()
    .AddOpenApiDefinitionStorage(myStorage);

var app = builder.Build();

app.UseRouting();
app.UseEndpoints(endpoints =>
{
    endpoints.MapOpenApi();
    endpoints.MapOpenApiEndpoints();
    endpoints.MapGraphQL();
});

app.Run();
```

`AddOpenApiDefinitionStorage()` registers the adapter services and provides the endpoint definitions. `AddGraphQLTransformer()` adds a document transformer that injects the generated endpoints into your OpenAPI specification. `MapOpenApiEndpoints()` registers the dynamic HTTP endpoints at runtime.

# Endpoint Definitions

Each REST endpoint is defined by a GraphQL operation annotated with an `@http` directive. You provide these operations through an `IOpenApiDefinitionStorage` implementation.

A GET endpoint that fetches a user by ID:

```graphql
"Fetches a user by their id"
query GetUserById($userId: ID!) @http(method: GET, route: "/users/{userId}") {
  userById(id: $userId) {
    id
    name
    email
  }
}
```

A POST endpoint that creates a user:

```graphql
"Creates a user"
mutation CreateUser($user: UserInput! @body)
@http(method: POST, route: "/users") {
  createUser(user: $user) {
    id
    name
    email
  }
}
```

The `@http` directive specifies the HTTP method and route. Route parameters like `{userId}` map to GraphQL variables. The `@body` directive on a variable indicates that the HTTP request body maps to that variable.

# How It Works

The adapter translates between REST and GraphQL concepts:

| REST Concept                 | GraphQL Concept                       |
| ---------------------------- | ------------------------------------- |
| HTTP method (GET, POST, PUT) | Specified by `@http(method: ...)`     |
| Route path                   | `@http(route: "/path/{param}")`       |
| Route parameters             | GraphQL variables matched by name     |
| Query parameters             | Variables listed in `queryParameters` |
| Request body                 | Variable annotated with `@body`       |
| Response body                | Selected fields from the operation    |

When a client sends an HTTP request to a generated endpoint, the adapter extracts route parameters, query parameters, and the request body, maps them to GraphQL variables, executes the operation, and returns the root field's data as the response body.

# Route Parameters

Route parameters in curly braces map to GraphQL variables by name:

```graphql
query GetUser($userId: ID!) @http(method: GET, route: "/users/{userId}") {
  userById(id: $userId) {
    id
    name
  }
}
```

A request to `GET /users/42` sets `$userId` to `"42"`.

You can also map route parameters to nested fields of a variable using the `key:$variable.path` syntax:

```graphql
mutation UpdateUser($user: UserInput! @body)
@http(method: PUT, route: "/users/{userId:$user.id}") {
  updateUser(user: $user) {
    id
    name
  }
}
```

A PUT request to `/users/42` with a JSON body sets the `id` field of the `$user` variable to `"42"`, and the rest of the body fills in the remaining fields.

# Query Parameters

Use the `queryParameters` argument on the `@http` directive to expose GraphQL variables as URL query parameters:

```graphql
query GetUserDetails($userId: ID!, $includeAddress: Boolean!)
@http(
  method: GET
  route: "/users/{userId}/details"
  queryParameters: ["includeAddress"]
) {
  userById(id: $userId) {
    id
    name
    address @include(if: $includeAddress) {
      street
    }
  }
}
```

A request to `#!http GET /users/1/details?includeAddress=true` sets `$includeAddress` to `true`.

Query parameters support the same `key:$variable.path` mapping syntax as route parameters.

# Request Body

The `@body` directive on a variable maps the entire HTTP request body to that variable:

```graphql
mutation CreateUser($user: UserInput! @body)
@http(method: POST, route: "/users") {
  createUser(user: $user) {
    id
    name
    email
  }
}
```

A POST request with a JSON body `#!json {"id": "6", "name": "Alice", "email": "alice@example.com"}` sets `$user` to that object. The request must have a `Content-Type: application/json` header.

# Shared Fragments

You can define reusable GraphQL fragments as separate documents. The adapter resolves fragment references across documents:

```graphql
# Document 1: endpoint definition
query GetUser($userId: ID!) @http(method: GET, route: "/users/{userId}") {
  userById(id: $userId) {
    ...UserFields
  }
}

# Document 2: shared fragment
fragment UserFields on User {
  id
  name
  email
  address {
    ...AddressFields
  }
}

# Document 3: another shared fragment
fragment AddressFields on Address {
  street
}
```

Each document is a separate entry in your `IOpenApiDefinitionStorage`. Fragment-only documents are treated as shared models.

## One model per document

A fragment-only document defines exactly one shared model. The **first** fragment in the document names the model, and that name is what other documents spread. Any further fragments in the same document are private helpers: fragments within that document can spread them, but no other document can reference them by name.

```graphql
# A model named "User", plus a helper only this document can spread
fragment User on User {
  ...UserContact
  id
  name
}

fragment UserContact on User {
  email
  address {
    street
  }
}
```

To make a fragment referenceable from another document, give it a document of its own.

## Fragment name uniqueness

Fragment names must be unique across every document that contributes to a single endpoint, which is the endpoint's own document plus each model it spreads, transitively. When two of those documents define a fragment with the same name, the endpoint's composed document is invalid and the endpoint responds with HTTP 500. Private helper fragments are not namespaced by their document, so this applies to them as well.

An endpoint that spreads a fragment no document defines is also invalid, and likewise responds with HTTP 500. In both cases the endpoint is still routed, and it is omitted from the OpenAPI specification. Register a diagnostic event listener (see [Diagnostics](#diagnostics)) to see the reason.

# Storage

The `IOpenApiDefinitionStorage` interface provides endpoint and fragment definitions to the adapter:

```csharp
using HotChocolate.Adapters.OpenApi;
using HotChocolate.Adapters.OpenApi.Storage;
using HotChocolate.Language;

public class MyOpenApiStorage : IOpenApiDefinitionStorage
{
    public ValueTask<IEnumerable<IOpenApiDefinition>>
        GetDefinitionsAsync(
            CancellationToken cancellationToken = default)
    {
        var documents = new List<IOpenApiDefinition>();

        var getUserDoc = Utf8GraphQLParser.Parse(
            """
            query GetUser($userId: ID!)
              @http(method: GET, route: "/users/{userId}") {
              userById(id: $userId) {
                id
                name
              }
            }
            """);
        documents.Add(OpenApiDefinitionParser.Parse(getUserDoc));

        return ValueTask.FromResult<IEnumerable<IOpenApiDefinition>>(
            documents);
    }

    // IOpenApiDefinitionStorage also extends IObservable, enabling
    // hot-reload when definitions change.
    public IDisposable Subscribe(
        IObserver<OpenApiDefinitionStorageEventArgs> observer)
        => /* your subscription logic */;
}
```

Register it with your GraphQL server:

```csharp
var storage = new MyOpenApiStorage();

builder
    .AddGraphQL()
    .AddQueryType<Query>()
    .AddOpenApiDefinitionStorage(storage);
```

The storage implements `IObservable<OpenApiDefinitionStorageEventArgs>`. When you push `Updated` or `Removed` events through this observable, the adapter picks up changes at runtime, adding, updating, or removing HTTP endpoints without a restart. This hot-reload behavior extends to the OpenAPI specification.

# Diagnostics

A definition that the adapter cannot use is skipped rather than throwing at startup, so an endpoint can be routed and still fail every call with HTTP 500. Derive from `OpenApiDiagnosticEventListener` to observe why:

```csharp
using HotChocolate.Adapters.OpenApi;

public class LoggingOpenApiDiagnosticEventListener(
    ILogger<LoggingOpenApiDiagnosticEventListener> logger)
    : OpenApiDiagnosticEventListener
{
    public override void ValidationErrors(
        IReadOnlyList<OpenApiDefinitionValidationError> errors)
    {
        foreach (var error in errors)
        {
            logger.LogError("{ValidationError}", error.Message);
        }
    }
}
```

Register it on the GraphQL server. The listener is activated from the schema services, so any application service it takes as a constructor argument, `ILogger<T>` included, must be made available with `AddApplicationService<T>()`:

```csharp
builder
    .AddGraphQL()
    .AddQueryType<Query>()
    .AddOpenApi()
    .AddApplicationService<ILogger<LoggingOpenApiDiagnosticEventListener>>()
    .AddDiagnosticEventListener<LoggingOpenApiDiagnosticEventListener>();
```

The listener receives an error for every definition the adapter cannot use: one that fails a validation rule, an endpoint whose composed document does not validate against the schema, an endpoint that could not be initialized, and a definition that could not be added to the OpenAPI document. Errors are reported whenever definitions are loaded or reloaded, so a hot-reload that breaks an endpoint reports it at that moment.

# OpenAPI Specification

The adapter integrates with ASP.NET Core's built-in OpenAPI support. After you register `#!csharp AddOpenApi(options => options.AddGraphQLTransformer())`, the generated endpoints appear in the OpenAPI document at `/openapi/v1.json`.

Each endpoint definition's description becomes the OpenAPI operation summary. Route and query parameters become OpenAPI parameters with types inferred from the GraphQL schema. Request body schemas are generated from the GraphQL input types.

# Fusion Integration

The OpenAPI adapter works with Fusion gateway servers. Replace `AddGraphQL()` with `AddGraphQLGateway()` and the rest of the configuration remains the same:

```csharp
builder
    .AddGraphQLGateway()
    .AddInMemoryConfiguration(compositeSchema)
    .AddHttpClientConfiguration("Subgraph", subgraphUri)
    .AddOpenApiDefinitionStorage(myStorage);
```

The Fusion gateway composes schemas from multiple subgraphs. The OpenAPI adapter generates REST endpoints that execute operations against the composed schema, so a single REST endpoint can fetch data from multiple subgraphs transparently.

# Next Steps

- [MCP](./mcp.md) to expose your GraphQL schema as MCP tools for AI agents.
- [Errors](../resolvers/errors.md) to customize error responses in generated endpoints.

---

### Website Content Docs Hotchocolate Defining A Schema Arguments (`website/content/docs/hotchocolate/defining-a-schema/arguments.md`)

---
title: "Arguments"
description: "Define field arguments in Hot Chocolate: resolver method parameters become GraphQL arguments, with default values, the [ID] attribute, and input object arguments."
---

GraphQL arguments let clients pass values to individual fields. In Hot Chocolate, each parameter on a resolver method becomes a field argument in the schema, unless it is a recognized service type (like `CancellationToken` or a registered service).

**GraphQL schema**

```graphql
type Query {
  user(id: ID!): User
  users(role: UserRole, limit: Int = 10): [User!]!
}
```

**Client query**

```graphql
{
  user(id: "UHJvZHVjdAppMQ==") {
    name
  }
}
```

Arguments are frequently provided through variables, which separate the static query structure from the dynamic runtime values:

```graphql
query ($userId: ID!) {
  user(id: $userId) {
    name
  }
}
```

# Defining Arguments

Method parameters on a resolver become GraphQL arguments.

<ExampleTabs>
<Implementation>

```csharp
[QueryType]
public static partial class UserQueries
{
    public static User? GetUser(string username, UserService users)
        => users.FindByName(username);
}
```

The `username` parameter becomes a `username: String!` argument. The `UserService` parameter is recognized as a service and is not exposed in the schema.

</Implementation>
<Code>

```csharp
public class UserQueriesType : ObjectType
{
    protected override void Configure(IObjectTypeDescriptor descriptor)
    {
        descriptor.Name(OperationTypeNames.Query);

        descriptor
            .Field("user")
            .Argument("username", a => a.Type<NonNullType<StringType>>())
            .Resolve(context =>
            {
                var username = context.ArgumentValue<string>("username");
                // ...
            });
    }
}
```

</Code>
</ExampleTabs>

# Renaming Arguments

Use `[GraphQLName]` to change the argument name in the schema while keeping the C# parameter name unchanged.

```csharp
[QueryType]
public static partial class UserQueries
{
    public static User? GetUser(
        [GraphQLName("name")] string username,
        UserService users)
        => users.FindByName(username);
}
```

This produces `#!sdl user(name: String!): User` in the schema.

# Optional Arguments

An argument is required when its C# type is non-nullable. Make an argument optional by using a nullable type.

```csharp
[QueryType]
public static partial class ProductQueries
{
    public static List<Product> GetProducts(string? category, int? limit)
    {
        // Both arguments are optional
        // ...
    }
}
```

This produces:

```graphql
type Query {
  products(category: String, limit: Int): [Product!]!
}
```

When using nullable reference types (recommended), `string` maps to `String!` and `string?` maps to `String`. See [Non-Null](./non-null.md) for details.

# Default Values

Use `[DefaultValue]` to assign a default to an argument. The default appears in the schema and is used when the client omits the argument.

```csharp
[QueryType]
public static partial class ProductQueries
{
    public static List<Product> GetProducts(
        [DefaultValue(10)] int limit)
    {
        // ...
    }
}
```

This produces `#!sdl products(limit: Int! = 10): [Product!]!`.

C# default parameter values also work:

```csharp
public static List<Product> GetProducts(int limit = 10)
```

For complex default values that cannot be expressed as C# constants (such as input objects), use `[DefaultValueSyntax]` with GraphQL value syntax:

```csharp
[QueryType]
public static partial class ProductQueries
{
    public static List<Product> GetProducts(
        [DefaultValueSyntax("{ title: null, year: 2024 }")] BookFilterInput filter)
    {
        // ...
    }
}
```

This produces `#!sdl products(filter: BookFilterInput! = { title: null, year: 2024 }): [Product!]!`. The string is parsed as a GraphQL value literal at schema build time.

# The ID Attribute

The `[ID]` attribute marks a parameter as a GraphQL `ID` scalar. When combined with [global object identification](./relay.md), it also deserializes the opaque global ID back to the underlying value.

```csharp
[QueryType]
public static partial class ProductQueries
{
    public static Product? GetProduct([ID] int id, CatalogContext db)
        => db.Products.Find(id);
}
```

To restrict the ID to a specific type (ensuring only IDs serialized for `Product` are accepted):

```csharp
public static Product? GetProduct(
    [ID(nameof(Product))] int id,
    CatalogContext db)
    => db.Products.Find(id);
```

You can also use the generic form `[ID<Product>]` which infers the type name automatically.

# Complex Arguments

When an argument needs multiple fields, use an [input object type](./input-object-types.md) instead of multiple scalar arguments.

```csharp
public record BookFilterInput(string? Title, string? Author, int? Year);

[QueryType]
public static partial class BookQueries
{
    public static List<Book> GetBooks(BookFilterInput filter, CatalogContext db)
    {
        // ...
    }
}
```

This produces:

```graphql
input BookFilterInput {
  title: String
  author: String
  year: Int
}

type Query {
  books(filter: BookFilterInput!): [Book!]!
}
```

# Next Steps

- **Need structured input?** See [Input Object Types](./input-object-types.md).
- **Need to understand nullability?** See [Non-Null](./non-null.md).
- **Need global IDs?** See [Relay](./relay.md).
- **Need to set up resolvers?** See [Resolvers](../resolvers/index.md).

---

### Website Content Docs Hotchocolate Defining A Schema Directives (`website/content/docs/hotchocolate/defining-a-schema/directives.md`)

---
title: "Directives"
description: "Create and apply GraphQL directives in Hot Chocolate, from built-ins like @skip and @include to custom directive types that alter execution behavior."
---

Directives let you add metadata for client tools (such as code generators and IDEs) or modify a GraphQL server’s runtime execution and type validation behavior.

There are two kinds of directives: executable directives, which annotate parts of GraphQL documents, and type-system directives, which annotate SDL types.

The GraphQL specification defines five built-in directives that every server must support:

| Directive      | Kind        | SDL                                                                                                                                                       |
| -------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@skip`        | Executable  | `#!sdl directive @skip(if: Boolean!) on FIELD \| FRAGMENT_SPREAD \| INLINE_FRAGMENT`                                                                      |
| `@include`     | Executable  | `#!sdl directive @include(if: Boolean!) on FIELD \| FRAGMENT_SPREAD \| INLINE_FRAGMENT`                                                                   |
| `@deprecated`  | Type-system | `#!sdl directive @deprecated(reason: String! = "No longer supported") on FIELD_DEFINITION \| ARGUMENT_DEFINITION \| INPUT_FIELD_DEFINITION \| ENUM_VALUE` |
| `@specifiedBy` | Type-system | `#!sdl directive @specifiedBy(url: String!) on SCALAR`                                                                                                    |
| `@oneOf`       | Type-system | `#!sdl directive @oneOf on INPUT_OBJECT`                                                                                                                  |

`@skip` and `@include` are executable directives used in queries to conditionally exclude or include fields. `@deprecated` marks schema elements as deprecated. `@specifiedBy` provides a URL pointing to the specification of a custom scalar type. `@oneOf` marks an input object as requiring exactly one of its fields to be set.

# Structure

Directives consist of a name and zero or more arguments. `@skip`, for example, has the name **skip** and a mandatory argument named **if**. Also, `@skip` carries a piece of hidden information only examinable in SDL, namely the location, which specifies where a directive is applicable. Let's take a look at the SDL of the `@skip` directive.

```sdl
directive @skip(if: Boolean!) on
    | FIELD
    | FRAGMENT_SPREAD
    | INLINE_FRAGMENT
```

The `directive` keyword in SDL indicates that we're dealing with a directive type declaration. The `@` sign also indicates that this is a directive but more from a usage perspective.

The word `skip` represents the directive's name followed by a pair of parentheses that includes a list of arguments, consisting, in our case, of one argument named `if` of type non-nullable boolean (meaning it is required).

The `on` keyword indicates the location where or at which part a directive is applicable, followed by a list of exact locations separated by pipes `|`. In the case of `@skip`, we can see that we're dealing with an executable directive because this directive is only applicable to fields, fragment-spreads, and inline-fragments.

# Usage

Let's say we have a GraphQL document and want to exclude details under certain circumstances; it would probably look something like this.

```graphql
query me($excludeDetails: Boolean!) {
  me {
    id
    name
    ...Details @skip(if: $excludeDetails)
  }
}

fragment Details on User {
  mobileNumber
  phoneNumber
}
```

With `@skip`, we've successfully altered the GraphQL's runtime execution behavior. If `$excludeDetails` is set to `true`, the execution engine will exclude the fields `mobileNumber` and `phoneNumber`; the response would look like this.

```json
{
  "data": {
    "me": {
      "id": "VXNlcgox",
      "name": "Henry"
    }
  }
}
```

Now that we know how to use directives in GraphQL, let's head over to the next section, which is about one crucial aspect of directives.

## Order Matters

**The order of directives is significant**, because the execution is in **sequential order**, which means one after the other. If we have something like the following example, we can see how directives can affect each other.

```graphql
query me {
  me {
    name @skip(if: true) @include(if: true)
  }
}
```

Since we excluded the field `name` in the first place, `@include` does not affect the field `name` anymore. We then just get an empty `me` object in return.

```json
{
  "data": {
    "me": {}
  }
}
```

> [!NOTE]
> We will have a deep dive on directives' order under the [Middleware](#order) section.

Now that we have a basic understanding of what directives are, how they work, and what we can do with them, let's create a custom directive.

# Custom Directives

To create a custom directive we need to define its name, location, and optionally its arguments. We also have to register the directive explicitly.

<ExampleTabs>
<Implementation>

Annotate a C# class with the `[DirectiveType]` attribute. The directive name is inferred from the class name (minus the "Directive" suffix if present). Public properties automatically become directive arguments.

```csharp
[DirectiveType(DirectiveLocation.Field)]
public class MyDirective
{
}
```

```csharp
builder.Services
    .AddGraphQLServer()
    .AddDirectiveType<MyDirective>();
```

</Implementation>
<Code>

Create a class that inherits from `DirectiveType` and override the `Configure` method.

```csharp
public class MyDirectiveType : DirectiveType
{
    protected override void Configure(IDirectiveTypeDescriptor descriptor)
    {
        descriptor.Name("my");
        descriptor.Location(DirectiveLocation.Field);
    }
}
```

```csharp
builder.Services
    .AddGraphQLServer()
    .AddDirectiveType<MyDirectiveType>();
```

</Code>
</ExampleTabs>

[Learn more about Locations](#locations)

We have registered a new directive named `my` without any arguments and limited the usage to fields only. A GraphQL query request with our new directive could look like this.

```graphql
query foo {
  bar @my
}
```

As of now, our custom directive provides no functionality. We will handle that part in the [Middleware](#middleware) section. But before that, let's talk about repeatable directives and arguments.

## Repeatable

By default, directives are not repeatable, which means directives are unique and can only be applied once at a specific location. For example, if we use the `my` directive twice at the field `bar`, we will encounter a validation error. So the following GraphQL query request results in an error if the directive is not repeatable.

```graphql
query foo {
  bar @my @my
}
```

We can enable repeatability like the following.

<ExampleTabs>
<Implementation>

Set `IsRepeatable = true` on the attribute.

```csharp
[DirectiveType(DirectiveLocation.Field, IsRepeatable = true)]
public class MyDirective
{
}
```

</Implementation>
<Code>

Call `Repeatable()` on the descriptor.

```csharp
public class MyDirectiveType : DirectiveType
{
    protected override void Configure(IDirectiveTypeDescriptor descriptor)
    {
        descriptor.Name("my");
        descriptor.Location(DirectiveLocation.Field);
        descriptor.Repeatable();
    }
}
```

</Code>
</ExampleTabs>

This configuration will translate into the following SDL.

```sdl
directive @my repeatable on FIELD
```

## Arguments

A directive can provide additional information through arguments. They might also come in handy, in combination with repeatable directives, for reusability purposes.

<ExampleTabs>
<Implementation>

Any public property on the class becomes a directive argument automatically.

```csharp
[DirectiveType(DirectiveLocation.FieldDefinition)]
public class MyDirective
{
    public string Name { get; set; }
}
```

</Implementation>
<Code>

Use a backing POCO with `DirectiveType<T>`. Public properties on the POCO are included as arguments implicitly.

```csharp
public class MyDirective
{
    public string Name { get; set; }
}

public class MyDirectiveType : DirectiveType<MyDirective>
{
    protected override void Configure(
        IDirectiveTypeDescriptor<MyDirective> descriptor)
    {
        descriptor.Name("my");
        descriptor.Location(DirectiveLocation.FieldDefinition);
    }
}
```

If we prefer to not use a backing POCO we can also use the `Argument()` method on the descriptor.

```csharp
public class MyDirectiveType : DirectiveType
{
    protected override void Configure(IDirectiveTypeDescriptor descriptor)
    {
        descriptor.Name("my");
        descriptor.Location(DirectiveLocation.Field);

        descriptor
            .Argument("name")
            .Type<NonNullType<StringType>>();
    }
}
```

</Code>
</ExampleTabs>

This configuration will translate into the following SDL.

```sdl
directive @my(name: String!) on FIELD
```

## Usage within Types

We could associate the `MyDirectiveType` with an object type like the following.

```csharp
public class FooType : ObjectType
{
    protected override void Configure(IObjectTypeDescriptor descriptor)
    {
        descriptor.Name("Foo");
        descriptor.Directive("my", new ArgumentNode("name", "bar"));
    }
}
```

> [!NOTE]
> For this to work the `MyDirectiveType` directive needs to have the appropriate location within the schema. In this example it would be `DirectiveLocation.Object`.

Referencing directives using their name is not type-safe and could lead to runtime errors, which are avoidable by using our generic variant of the directive type.

Once we have defined our directive using `DirectiveType<T>`, we can pass an instance of the backing POCO (`<T>`) instead of the name of the directive and an `ArgumentNode`.

```csharp
public class FooType : ObjectType
{
    protected override void Configure(IObjectTypeDescriptor descriptor)
    {
        descriptor.Name("Foo");
        descriptor.Directive(new MyDirective { Name = "bar" });
    }
}
```

Since the directive instance that we have added to our type is now a strong .NET type, we don't have to fear changes to the directive structure or name anymore.

## Directives on Directive Definitions

A directive definition is itself a schema element, so it can carry directives. You can use this to mark a directive definition as deprecated or to attach metadata to it, in the same way you annotate object types, fields, or enum values.

To apply a directive to a directive definition, that directive must declare the `DIRECTIVE_DEFINITION` location.

### Declaring a Directive That Targets Directive Definitions

A directive can only be applied to a directive definition when its own definition includes the `DIRECTIVE_DEFINITION` location.

<ExampleTabs>
<Implementation>

```csharp
[DirectiveType(DirectiveLocation.DirectiveDefinition)]
public class OnDirectiveDefinition
{
}
```

</Implementation>
<Code>

```csharp
public class OnDirectiveDefinitionType : DirectiveType
{
    protected override void Configure(IDirectiveTypeDescriptor descriptor)
    {
        descriptor.Name("onDirectiveDefinition");
        descriptor.Location(DirectiveLocation.DirectiveDefinition);
    }
}
```

</Code>
</ExampleTabs>

This configuration translates into the following SDL.

```sdl
directive @onDirectiveDefinition on DIRECTIVE_DEFINITION
```

### Applying a Directive to a Directive Definition

Once a directive declares the `DIRECTIVE_DEFINITION` location, you can apply it to another directive definition.

In schema-first SDL you place the applied directives after the argument definitions (if any) and before the optional `repeatable` keyword and the `on` keyword.

```sdl
directive @onDirectiveDefinition on DIRECTIVE_DEFINITION

directive @custom @onDirectiveDefinition on OBJECT
```

In code-first, call `Directive(...)` on the `IDirectiveTypeDescriptor` to apply a directive to the directive definition you are configuring.

```csharp
public class CustomDirectiveType : DirectiveType
{
    protected override void Configure(IDirectiveTypeDescriptor descriptor)
    {
        descriptor.Name("custom");
        descriptor.Location(DirectiveLocation.Object);
        descriptor.Directive("onDirectiveDefinition");
    }
}
```

The descriptor offers the following overloads to apply a directive to the directive definition: `Directive(string name, params ArgumentNode[] arguments)`, `Directive<T>(T instance)`, and `Directive<T>()`.

> [!NOTE]
> Applying a custom directive to a directive definition is done through the descriptor (Code) or through schema-first SDL.

### Deprecating a Directive Definition

`@deprecated` is allowed on directive definitions and on their arguments. Use it to signal that a directive (or one of its arguments) should no longer be used.

<ExampleTabs>
<Implementation>

Annotate the directive class with `[Obsolete(...)]` or `[GraphQLDeprecated(...)]`. Both set the deprecation.

```csharp
[Obsolete("Use @custom instead.")]
[DirectiveType(DirectiveLocation.Object)]
public class OldDirective
{
}
```

```csharp
[GraphQLDeprecated("Use @custom instead.")]
[DirectiveType(DirectiveLocation.Object)]
public class OldDirective
{
}
```

</Implementation>
<Code>

Call `Deprecated(...)` on the descriptor.

```csharp
public class OldDirectiveType : DirectiveType
{
    protected override void Configure(IDirectiveTypeDescriptor descriptor)
    {
        descriptor.Name("old");
        descriptor.Location(DirectiveLocation.Object);
        descriptor.Deprecated("Use @custom instead.");
    }
}
```

</Code>
</ExampleTabs>

In schema-first SDL, apply `@deprecated` to the directive definition or to one of its arguments.

```sdl
directive @old @deprecated(reason: "Use @custom.") on OBJECT

directive @custom(
  legacyArg: Int @deprecated(reason: "Use newArg instead.")
  newArg: String
) on OBJECT
```

### Extending a Directive

In schema-first you can use `extend directive` to add directives, including a deprecation, to an existing directive definition. The added directives merge into the existing definition.

```sdl
directive @custom on OBJECT

extend directive @custom @onDirectiveDefinition

extend directive @custom @deprecated(reason: "Use something else.")
```

### Introspection

Introspection exposes this surface, mirroring how deprecated fields, enum values, and arguments behave.

- `__Directive` exposes `isDeprecated: Boolean!` and `deprecationReason: String`.
- `#!sdl __Schema.directives(includeDeprecated: Boolean = false)` hides deprecated directives by default. Pass `includeDeprecated: true` to include them.
- `__DirectiveLocation` includes `DIRECTIVE_DEFINITION`.

```graphql
{
  __schema {
    directives(includeDeprecated: true) {
      name
      isDeprecated
      deprecationReason
      locations
    }
  }
}
```

### Troubleshooting

**Problem:** The directive definition `@custom` must not reference itself.

- **Cause:** A directive is applied to its own definition or to one of its own arguments. Self-reference is not allowed.
- **Solution:** Apply a different directive, or remove the self-application.

**Problem:** The specified directive `@onObject` is not allowed on the current location `DirectiveDefinition`.

- **Cause:** The applied directive's definition does not include the `DIRECTIVE_DEFINITION` location.
- **Solution:** Add `DIRECTIVE_DEFINITION` to that directive's locations (`on DIRECTIVE_DEFINITION` in SDL, or `descriptor.Location(DirectiveLocation.DirectiveDefinition)` in code-first).

**Problem:** The directive extension `#!sdl extend directive @unknown` targets an undefined directive.

- **Cause:** `extend directive` references a directive that is not defined.
- **Solution:** Define the directive before extending it.

## Locations

A directive can define one or multiple locations, where it can be applied. Multiple locations are separated by a pipe `|`.

```csharp
descriptor.Location(DirectiveLocation.Field | DirectiveLocation.Object);
```

Generally we distinguish between two types of locations: Type system and executable locations.

### Type System Locations

Type system locations specify where we can place a specific directive in the schema. The arguments of directives specified in these locations are fixed. We can query such directives through introspection.

The following schema shows where type system directives can be applied.

```sdl
directive @schema on SCHEMA
directive @object on OBJECT
directive @argumentDefinition on ARGUMENT_DEFINITION
directive @fieldDefinition on FIELD_DEFINITION
directive @inputObject on INPUT_OBJECT
directive @inputFieldDefinition on INPUT_FIELD_DEFINITION
directive @interface on INTERFACE
directive @enum on ENUM
directive @enumValue on ENUM_VALUE
directive @union on UNION
directive @scalar on SCALAR
directive @directiveDefinition on DIRECTIVE_DEFINITION
directive @custom @directiveDefinition on OBJECT
schema @schema {
  query: Query
}
type Query @object {
  search(by: SearchInput! @argumentDefinition): SearchResult @fieldDefinition
}
input SearchInput @inputObject {
  searchTerm: String @inputFieldDefinition
}
interface HasDescription @interface {
  description: String
}
type Product implements HasDescription {
  added: DateTime
  description: String
}
enum UserKind @enum {
  Administrator @enumValue
  Moderator
}
type User {
  name: String
  userKind: UserKind
}
union SearchResult @union = Product | User
scalar DateTime @scalar
```

The `DIRECTIVE_DEFINITION` location lets a directive be applied to other directive definitions, as with `@directiveDefinition` on the `@custom` definition above. See [Directives on directive definitions](#directives-on-directive-definitions) for the details.

### Executable Locations

Executable locations specify where a client can place a specific directive, when executing an operation.

Our server defines the following directives.

```sdl
directive @query on QUERY
directive @field on FIELD
directive @fragmentSpread on FRAGMENT_SPREAD
directive @inlineFragment on INLINE_FRAGMENT
directive @fragmentDefinition on FRAGMENT_DEFINITION
directive @mutation on MUTATION
directive @subscription on SUBSCRIPTION
```

The following request document shows where we, as a client, can apply these directives.

```graphql
query getUsers @query {
  search(by: { searchTerm: "Foo" }) @field {
    ...DescriptionFragment @fragmentSpread
    ... on User @inlineFragment {
      userKind
    }
  }
}

fragment DescriptionFragment on HasDescription @fragmentDefinition {
  description
}

mutation createNewUser @mutation {
  createUser(input: { name: "Ada Lovelace" }) {
    user {
      name
    }
  }
}

subscription subscribeToUser @subscription {
  onUserChanged(id: 1) {
    user {
      name
    }
  }
}
```

## Middleware

What makes directives in Hot Chocolate very useful is the ability to associate a middleware with it. A middleware can alternate the result, or even produce the result, of a field. A directive middleware is only added to a field middleware pipeline when the directive was annotated to the object definition, the field definition or the field.

Moreover, if the directive is repeatable the middleware will be added multiple times to the middleware allowing to build a real pipeline with it.

In order to add a middleware to a directive we could declare it with the descriptor as a delegate.

```csharp
public class MyDirectiveType : DirectiveType<MyDirective>
{
    protected override void Configure(
        IDirectiveTypeDescriptor<MyDirective> descriptor)
    {
        descriptor.Name("my");
        descriptor.Location(DirectiveLocation.Object);

        descriptor.Use((next, directive) => context =>
        {
            context.Result = "Bar";
            return next.Invoke(context);
        });
    }
}
```

Directives with middleware or executable directives can be put on object types and on their field definitions or on the field selection in a query. Executable directives on an object type will replace the field resolver of every field of the annotated object type.

### Order

In GraphQL the order of directives is significant and with our middleware we use this order to create a resolver pipeline through which the result flows.

The resolver pipeline consists of a sequence of directive delegates, called one after the other.

Each delegate can perform operations before and after the next delegate. A delegate can also decide to not pass a resolver request to the next delegate, which is called short-circuiting the resolver pipeline.
Short-circuiting is often desirable because it avoids unnecessary work.

The order of the middleware pipeline is defined by the order of the directives. Since executable directives will flow from the object type to its field definitions, the directives of the type would be called first in the order that they were annotated.

```sdl
type Query {
  foo: Bar
}

type Bar @a @b {
  baz: String @c @d
}
```

So, the directives in the above example would be called in the following order `a, b, c, d`.

If there were more directives in the query, they would be appended to the directives from the type.

```graphql
{
  foo {
    baz @e @f
  }
}
```

So, now the order would be like the following: `a, b, c, d, e, f`.

Every middleware can execute the original resolver function by calling `ResolveAsync()` on the `IDirectiveContext`.

---

### Website Content Docs Hotchocolate Defining A Schema Documentation (`website/content/docs/hotchocolate/defining-a-schema/documentation.md`)

---
title: "Documentation"
description: "Add GraphQL schema descriptions in Hot Chocolate with the [GraphQLDescription] attribute or XML documentation comments, visible in tooling and introspection."
---

GraphQL descriptions enrich your schema with information that consumers see in developer tools, IDE autocompletion, and introspection results. Every type, field, argument, and enum value can carry a description string.

```graphql
"Represents a registered user."
type User {
  "The unique username."
  username: String!
}
```

Hot Chocolate provides two ways to add descriptions: the `[GraphQLDescription]` attribute and XML documentation comments.

# Using GraphQLDescription

The `[GraphQLDescription]` attribute sets a description on any schema element.

<ExampleTabs>
<Implementation>

```csharp
[GraphQLDescription("Represents a registered user.")]
public class User
{
    [GraphQLDescription("The unique username.")]
    public string Username { get; set; }
}

[GraphQLDescription("Available user roles.")]
public enum UserRole
{
    [GraphQLDescription("Full system access.")]
    Administrator,

    [GraphQLDescription("Content moderation access.")]
    Moderator
}

[QueryType]
public static partial class UserQueries
{
    [GraphQLDescription("Finds a user by username.")]
    public static User? GetUser(
        [GraphQLDescription("The username to search for.")] string username,
        UserService users)
        => users.FindByName(username);
}
```

</Implementation>
<Code>

```csharp
public class UserType : ObjectType<User>
{
    protected override void Configure(IObjectTypeDescriptor<User> descriptor)
    {
        descriptor.Description("Represents a registered user.");

        descriptor
            .Field(f => f.Username)
            .Description("The unique username.");
    }
}

public class UserRoleType : EnumType<UserRole>
{
    protected override void Configure(IEnumTypeDescriptor<UserRole> descriptor)
    {
        descriptor.Description("Available user roles.");

        descriptor
            .Value(UserRole.Administrator)
            .Description("Full system access.");
    }
}
```

In code-first, the `Description()` method takes precedence over all other forms of documentation. This applies even if the provided value is `null` or empty.

</Code>
</ExampleTabs>

# Using XML Documentation Comments

Hot Chocolate generates descriptions from standard C# XML documentation comments. The source generator extracts them at build time, so your C# code is the single source of documentation for both your code and GraphQL schema.

```csharp
/// <summary>
/// Represents a registered user.
/// </summary>
public class User
{
    /// <summary>
    /// The unique username.
    /// </summary>
    public string Username { get; set; }
}

/// <summary>
/// Available user roles.
/// </summary>
public enum UserRole
{
    /// <summary>
    /// Full system access.
    /// </summary>
    Administrator,

    /// <summary>
    /// Content moderation access.
    /// </summary>
    Moderator
}

[QueryType]
public static partial class UserQueries
{
    /// <summary>
    /// Finds a user by username.
    /// </summary>
    /// <param name="username">The username to search for.</param>
    public static User? GetUser(string username, UserService users)
        => users.FindByName(username);
}
```

## Source Generator (Default)

Hot Chocolate's source generator extracts XML documentation comments directly from the source code during compilation. This is the default behavior, no additional project configuration is required.

The source generator reads `<summary>`, `<param>`, `<returns>`, and `<exception>` tags and embeds the extracted text into the generated type configuration. Because this happens at build time, you do not need to ship XML documentation files with your application.

### Supported Tags

| Tag                                 | Usage                                                                |
| ----------------------------------- | -------------------------------------------------------------------- |
| `<summary>`                         | Sets the description of the type, field, or enum value.              |
| `<param name="...">`                | Sets the description of a field argument.                            |
| `<returns>`                         | Appended to the field description under a **Returns** heading.       |
| `<exception cref="..." code="...">` | Appended under an **Errors** heading. Requires the `code` attribute. |
| `<inheritdoc />`                    | Resolves documentation from a base class or interface.               |
| `<inheritdoc cref="..." />`         | Resolves documentation from a specific member.                       |

### Example with Returns and Errors

```csharp
[QueryType]
public static partial class UserQueries
{
    /// <summary>
    /// Finds a user by their unique username.
    /// </summary>
    /// <param name="username">The username to search for.</param>
    /// <returns>The matching user, or null if not found.</returns>
    /// <exception cref="Exception" code="NOT_AUTHORIZED">
    /// The caller does not have permission to search users.
    /// </exception>
    public static User? GetUser(string username, UserService users)
        => users.FindByName(username);
}
```

This produces a field description that includes the summary, a **Returns** section, and an **Errors** section.

### Disabling Source Generator Documentation

To prevent the source generator from extracting XML documentation, add the `Module` attribute with the `DisableXmlDocumentation` option:

```csharp
using HotChocolate;

[assembly: Module("MyModule", ModuleOptions.Default | ModuleOptions.DisableXmlDocumentation)]
```

When `DisableXmlDocumentation` is set, `[GraphQLDescription]` attributes continue to work. Only the automatic extraction of XML comments is suppressed.

## Runtime XML Documentation

If you are not using the source generator or need XML documentation from referenced assemblies outside the source generator's scope, Hot Chocolate can read XML documentation files at runtime. Enable `GenerateDocumentationFile` in your `.csproj`:

```xml
<PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```

The `<NoWarn>` element is optional. It suppresses compiler warnings for types without documentation comments.

### Disabling Runtime XML Documentation

If you do not want runtime XML comments to appear in the schema:

```csharp
builder
    .AddGraphQL()
    .ModifyOptions(opt => opt.UseXmlDocumentation = false);
```

# Priority Order

When both `[GraphQLDescription]` and XML documentation are present, they follow this priority:

1. **`[GraphQLDescription]` attribute** (implementation-first): Used if the value is non-null and non-empty. If null or empty, XML documentation is used as a fallback.
2. **`Description()` method** (code-first): Always takes precedence, even if null or empty.
3. **XML documentation comments**: Used as a fallback when no explicit description is set.

# Custom Naming Conventions

If you use a custom naming convention and runtime XML documentation, pass an `XmlDocumentationProvider` to the convention so descriptions are preserved. This does not apply when using the source generator, which handles documentation extraction at build time.

```csharp
public class CustomNamingConventions : DefaultNamingConventions
{
    public CustomNamingConventions(
        IDocumentationProvider documentationProvider)
        : base(documentationProvider) { }
}
```

```csharp
IReadOnlySchemaOptions capturedSchemaOptions;

builder
    .AddGraphQL()
    .ModifyOptions(opt => capturedSchemaOptions = opt)
    .AddConvention<INamingConventions>(sp =>
        new CustomNamingConventions(
            new XmlDocumentationProvider(
                new XmlDocumentationFileResolver(
                    capturedSchemaOptions.ResolveXmlDocumentationFileName),
                sp.GetApplicationService<ObjectPool<StringBuilder>>()
                    ?? new NoOpStringBuilderPool())));
```

# Next Steps

- **Need to deprecate fields?** See [Versioning](./versioning.md).
- **Need to define enums?** See [Enums](./enums.md).
- **Need to define object types?** See [Object Types](./object-types.md).

---

### Website Content Docs Hotchocolate Defining A Schema Dynamic Schemas (`website/content/docs/hotchocolate/defining-a-schema/dynamic-schemas.md`)

---
title: "Dynamic Schemas"
description: "Build dynamic GraphQL schemas in Hot Chocolate with `ITypeModule`: provide types at runtime and trigger hot schema reloads when configuration changes."
---

In multi-tenant or CMS-like applications, the GraphQL schema may need to change at runtime based on configuration, database structure, or per-tenant requirements. Hot Chocolate supports dynamic schemas through the `ITypeModule` interface, which lets you provide types programmatically and trigger schema reloads when the underlying data changes.

# The ITypeModule Interface

`ITypeModule` is the entry point for dynamically providing types to the schema building process. It has two members:

- `TypesChanged`: An event that signals when types have changed and the current schema should be replaced.
- `CreateTypesAsync`: A method called during schema construction to create types for the new schema instance.

When you fire the `TypesChanged` event, Hot Chocolate phases out the old schema and builds a new one using the updated types from your module. This gives you hot-reload behavior without restarting the application.

```csharp
builder
    .AddGraphQL()
    .AddTypeModule<MyTypeModule>();
```

# Creating Types from a JSON File

This example reads type definitions from a JSON file. In a real application, the JSON might come from a database, an admin UI, or an external configuration service.

```csharp
public class JsonTypeModule : ITypeModule
{
    private readonly string _file;

    public JsonTypeModule(string file)
    {
        _file = file;
    }

    public event EventHandler<EventArgs>? TypesChanged;

    public async ValueTask<IReadOnlyCollection<ITypeSystemMember>> CreateTypesAsync(
        IDescriptorContext context,
        CancellationToken cancellationToken)
    {
        var types = new List<ITypeSystemMember>();

        await using var file = File.OpenRead(_file);
        using var json = await JsonDocument.ParseAsync(
            file, cancellationToken: cancellationToken);

        foreach (var type in json.RootElement.EnumerateArray())
        {
            var typeDefinition = new ObjectTypeDefinition(
                type.GetProperty("name").GetString()!);

            foreach (var field in type.GetProperty("fields").EnumerateArray())
            {
                typeDefinition.Fields.Add(
                    new ObjectFieldDefinition(
                        field.GetString()!,
                        type: TypeReference.Parse("String!"),
                        pureResolver: ctx => "foo"));
            }

            types.Add(
                type.GetProperty("extension").GetBoolean()
                    ? ObjectTypeExtension.CreateUnsafe(typeDefinition)
                    : ObjectType.CreateUnsafe(typeDefinition));
        }

        return types;
    }
}
```

When the JSON file changes, call `TypesChanged` to trigger a schema rebuild. You could use a file watcher or a polling mechanism to detect changes.

# Unsafe Type Creation

The `CreateUnsafe` method creates types directly from definition objects, bypassing the standard descriptor API. This gives you full control over the type structure but requires understanding the Hot Chocolate type system internals.

## Creating an Object Type

```csharp
var objectTypeDef = new ObjectTypeDefinition("Product")
{
    Description = "Represents a product in the catalog.",
    RuntimeType = typeof(Dictionary<string, object>)
};

var idField = new ObjectFieldDefinition(
    "id",
    "Unique identifier for the product.",
    TypeReference.Parse("ID!"),
    pureResolver: ctx => ctx.Parent<Dictionary<string, object>>()["id"]);

var nameField = new ObjectFieldDefinition(
    "name",
    "Name of the product.",
    TypeReference.Parse("String!"),
    pureResolver: ctx => ctx.Parent<Dictionary<string, object>>()["name"]);

objectTypeDef.Fields.Add(idField);
objectTypeDef.Fields.Add(nameField);

var productType = ObjectType.CreateUnsafe(objectTypeDef);
```

## Adding Fields with Arguments

```csharp
var discountArg = new ArgumentDefinition(
    "discount",
    "Discount percentage to apply.",
    TypeReference.Parse("Float!"));

var discountPriceField = new ObjectFieldDefinition(
    "discountPrice",
    "Price after discount.",
    TypeReference.Parse("Float!"),
    pureResolver: ctx =>
    {
        var product = ctx.Parent<Dictionary<string, object>>();
        var discountPct = ctx.ArgumentValue<float>("discount");
        var price = (float)product["price"];
        return price * (1 - discountPct / 100);
    })
{
    Arguments = { discountArg }
};

objectTypeDef.Fields.Add(discountPriceField);
```

## Creating an Input Object Type

```csharp
var inputTypeDef = new InputObjectTypeDefinition("ProductInput")
{
    Description = "Input for creating or updating a product.",
    RuntimeType = typeof(Dictionary<string, object>)
};

inputTypeDef.Fields.Add(new InputFieldDefinition(
    "name", "Name of the product.", TypeReference.Parse("String!")));

inputTypeDef.Fields.Add(new InputFieldDefinition(
    "price", "Price of the product.", TypeReference.Parse("Float!")));

var productInputType = InputObjectType.CreateUnsafe(inputTypeDef);
```

## Resolver Types

Hot Chocolate supports two resolver delegate types for dynamically created fields:

**Async resolvers** handle asynchronous operations like database queries or service calls:

```csharp
var reviewsField = new ObjectFieldDefinition(
    "reviews",
    "Reviews for the product.",
    TypeReference.Parse("[Review!]"),
    resolver: async ctx =>
    {
        var productId = ctx.Parent<Dictionary<string, object>>()["id"];
        var service = ctx.Service<IReviewService>();
        return await service.GetReviewsAsync(productId);
    });
```

**Pure resolvers** handle synchronous, side-effect-free operations. The execution engine optimizes these for better performance:

```csharp
var nameField = new ObjectFieldDefinition(
    "name",
    "Name of the product.",
    TypeReference.Parse("String!"),
    pureResolver: ctx => ctx.Parent<Dictionary<string, object>>()["name"]);
```

Use pure resolvers when you do not need async operations or service access. Use async resolvers when you need to call services, databases, or perform any I/O.

## Combining Types in a Mutation

```csharp
var createProductField = new ObjectFieldDefinition(
    "createProduct",
    "Creates a new product.",
    TypeReference.Parse("Product!"),
    resolver: async ctx =>
    {
        var input = ctx.ArgumentValue<Dictionary<string, object>>("input");
        var service = ctx.Service<IProductService>();
        return await service.CreateProductAsync(input);
    })
{
    Arguments =
    {
        new ArgumentDefinition(
            "input",
            "Input for creating the product.",
            TypeReference.Parse("ProductInput!"))
    }
};

var mutationDef = new ObjectTypeDefinition("Mutation")
{
    RuntimeType = typeof(object)
};
mutationDef.Fields.Add(createProductField);

var mutationType = ObjectType.CreateUnsafe(mutationDef);

builder
    .AddGraphQL()
    .AddQueryType()
    .AddMutationType(mutationType)
    .AddType(productInputType)
    .AddType(productType);
```

# Next Steps

- **Need to extend existing types?** See [Extending Types](./object-types.md).
- **Need to define types with the descriptor API?** See [Object Types](./object-types.md).
- **Need to understand type modules in depth?** Explore the `ITypeModule` interface in the Hot Chocolate source code under `src/HotChocolate/Core/src/Types/`.

---

### Website Content Docs Hotchocolate Defining A Schema Enums (`website/content/docs/hotchocolate/defining-a-schema/enums.md`)

---
title: "Enums"
description: "Define GraphQL enum types in Hot Chocolate: C# enums map automatically, and `EnumType` descriptors let you rename values and control value binding."
---

A GraphQL enum is a special scalar restricted to a fixed set of allowed values. Enums work as both input and output types. In C#, a standard `enum` maps directly to a GraphQL enum type.

**GraphQL schema**

```graphql
enum UserRole {
  GUEST
  STANDARD
  ADMINISTRATOR
}

type Query {
  role: UserRole
  usersByRole(role: UserRole): [User]
}
```

**Client query**

```graphql
{
  usersByRole(role: ADMINISTRATOR) {
    id
  }
}
```

When an enum appears in a JSON response or in a variables object, it is represented as a string (`"ADMINISTRATOR"`). When used directly in a query argument, it is a literal without quotes (`ADMINISTRATOR`).

# Defining an Enum Type

Hot Chocolate picks up any C# `enum` that appears in a resolver's return type or parameters and exposes it as a GraphQL enum.

<ExampleTabs>
<Implementation>

```csharp
public enum UserRole
{
    Guest,
    Standard,
    Administrator
}

[QueryType]
public static partial class UserQueries
{
    public static User[] GetUsersByRole(UserRole role)
    {
        // ...
    }
}
```

No extra registration is needed. The source generator discovers `UserRole` through the resolver parameter.

</Implementation>
<Code>

```csharp
public enum UserRole
{
    Guest,
    Standard,
    Administrator
}

public class UserRoleType : EnumType<UserRole>
{
}
```

Code-first enum types are not automatically inferred because multiple `EnumType<UserRole>` subclasses could exist with different configurations. Register the type explicitly or specify it on a per-field basis:

```csharp
builder
    .AddGraphQL()
    .AddType<UserRoleType>();
```

</Code>
</ExampleTabs>

# Naming Conventions

Hot Chocolate converts C# enum member names to `UPPER_SNAKE_CASE` following the GraphQL convention:

| C# member          | GraphQL value        |
| ------------------ | -------------------- |
| `Guest`            | `GUEST`              |
| `HeadOfDepartment` | `HEAD_OF_DEPARTMENT` |

The enum type name defaults to the C# type name (`UserRole`).

## Overriding Names

Use `[GraphQLName]` to set an explicit name on the type or individual values.

<ExampleTabs>
<Implementation>

```csharp
[GraphQLName("Role")]
public enum UserRole
{
    [GraphQLName("VISITOR")]
    Guest,
    Standard,
    Administrator
}
```

</Implementation>
<Code>

```csharp
public class UserRoleType : EnumType<UserRole>
{
    protected override void Configure(IEnumTypeDescriptor<UserRole> descriptor)
    {
        descriptor.Name("Role");

        descriptor.Value(UserRole.Guest).Name("VISITOR");
    }
}
```

</Code>
</ExampleTabs>

Both approaches produce the following schema:

```graphql
enum Role {
  VISITOR
  STANDARD
  ADMINISTRATOR
}
```

# Ignoring Values

You can exclude individual enum members from the GraphQL schema.

<ExampleTabs>
<Implementation>

```csharp
public enum UserRole
{
    [GraphQLIgnore]
    Internal,
    Guest,
    Standard,
    Administrator
}
```

</Implementation>
<Code>

```csharp
public class UserRoleType : EnumType<UserRole>
{
    protected override void Configure(IEnumTypeDescriptor<UserRole> descriptor)
    {
        descriptor.Ignore(UserRole.Internal);
    }
}
```

</Code>
</ExampleTabs>

# Binding to Non-Enum Types

In code-first, you can bind an enum type to any .NET type, such as `string`.

```csharp
public class UserRoleType : EnumType<string>
{
    protected override void Configure(IEnumTypeDescriptor<string> descriptor)
    {
        descriptor.Name("UserRole");

        descriptor
            .Value("Default")
            .Name("STANDARD");
    }
}
```

This is useful when enum values come from configuration or a database rather than a compile-time C# enum.

# Next Steps

- **Need to define output types?** See [Object Types](./object-types.md).
- **Need nullable or required fields?** See [Non-Null](./non-null.md).
- **Need to document enum values?** See [Documentation](./documentation.md).
- **Need to deprecate an enum value?** See [Versioning](./versioning.md).

---

### Website Content Docs Hotchocolate Defining A Schema Input Object Types (`website/content/docs/hotchocolate/defining-a-schema/input-object-types.md`)

---
title: "Input Object Types"
metaTitle: "GraphQL Input Object Types in Hot Chocolate"
description: "Define GraphQL input object types in Hot Chocolate to pass structured arguments, with records, default values, optional properties, and @oneOf inputs."
---

GraphQL input object types let you pass structured data as arguments. While [scalar arguments](./arguments.md) work for simple values, input types let you group related fields into a single object. Input types differ from output object types: their fields cannot have arguments and they use the `input` keyword in the schema.

**GraphQL schema**

```graphql
input CreateBookInput {
  title: String!
  author: String!
}

type Mutation {
  createBook(input: CreateBookInput!): Book
}
```

# Defining an Input Type

Any C# class or record used as a resolver parameter (that is not a scalar, enum, or service) becomes an input object type in the schema.

<ExampleTabs>
<Implementation>

```csharp
public class CreateBookInput
{
    public string Title { get; set; }
    public string Author { get; set; }
}

[MutationType]
public static partial class BookMutations
{
    public static async Task<Book> CreateBookAsync(
        CreateBookInput input,
        CatalogContext db,
        CancellationToken ct)
    {
        var book = new Book { Title = input.Title, Author = input.Author };
        db.Books.Add(book);
        await db.SaveChangesAsync(ct);
        return book;
    }
}
```

If a class used as an argument does not end in `Input`, Hot Chocolate appends `Input` to the type name in the schema automatically.

</Implementation>
<Code>

```csharp
public class CreateBookInput
{
    public string Title { get; set; }
    public string Author { get; set; }
}

public class CreateBookInputType : InputObjectType<CreateBookInput>
{
    protected override void Configure(
        IInputObjectTypeDescriptor<CreateBookInput> descriptor)
    {
        descriptor.Field(f => f.Title).Type<NonNullType<StringType>>();
        descriptor.Field(f => f.Author).Type<NonNullType<StringType>>();
    }
}
```

</Code>
</ExampleTabs>

# Using Records and Immutable Types

Input types can use immutable classes or C# records. Hot Chocolate calls the constructor instead of setting properties. The rules are:

1. Each constructor parameter type must match the corresponding property type.
2. Each constructor parameter name must match the property name (with a lowercase first letter).
3. All properties must have a matching constructor parameter.

Hot Chocolate validates input constructors at schema build time, so mismatches are caught early.

```csharp
public record CreateBookInput(string Title, string Author);
```

This record is equivalent to a class with a constructor and get-only properties.

# Default Values

The `[DefaultValue]` attribute assigns a default value to an input field. When the client omits the field, the default is used.

<ExampleTabs>
<Implementation>

```csharp
public class UserFilterInput
{
    public string? Name { get; set; }

    [DefaultValue(true)]
    public bool IsActive { get; set; }
}
```

This produces:

```graphql
input UserFilterInput {
  name: String
  isActive: Boolean! = true
}
```

</Implementation>
<Code>

```csharp
public class UserFilterInputType : InputObjectType<UserFilterInput>
{
    protected override void Configure(
        IInputObjectTypeDescriptor<UserFilterInput> descriptor)
    {
        descriptor.Field(f => f.IsActive).DefaultValue(true);
    }
}
```

</Code>
</ExampleTabs>

Default values maintain backward compatibility. When you add a new field to an input type, providing a default value keeps existing queries working.

## Default Values with GraphQL Syntax

For complex defaults (objects or lists), use `[DefaultValueSyntax]` with GraphQL value literal syntax.

```csharp
public class UserProfileInput
{
    public string? Name { get; set; }

    [DefaultValueSyntax("{ notifications: true, theme: \"light\" }")]
    public Preferences? Preferences { get; set; }
}
```

In code-first, use the `DefaultValueSyntax` method:

```csharp
descriptor
    .Field(f => f.Preferences)
    .DefaultValueSyntax("{ notifications: true, theme: \"light\" }");
```

# Optional Properties

Use `Optional<T>` to distinguish between a field that was not provided and a field explicitly set to `null`. This is important for partial updates where you need to know whether the client intended to clear a value. A field set to a variable that the request does not provide counts as not provided.

```csharp
public class UpdateBookInput
{
    [DefaultValue("")]
    public Optional<string> Title { get; set; }

    public string Author { get; set; }
}
```

When using `Optional<T>` on a non-nullable field, you must add `[DefaultValue]` to make the field optional in the schema.

Records work too:

```csharp
public record UpdateBookInput(
    [property: DefaultValue("")] Optional<string> Title,
    string Author);
```

# OneOf Input Objects

A `@oneOf` input type requires that exactly one field is set and non-null. This provides input polymorphism, letting a single argument accept different shapes of data.

**GraphQL schema**

```graphql
input PetInput @oneOf {
  cat: CatInput
  dog: DogInput
}
```

<ExampleTabs>
<Implementation>

```csharp
[OneOf]
public class PetInput
{
    public CatInput? Cat { get; set; }
    public DogInput? Dog { get; set; }
}

public class CatInput
{
    public string Name { get; set; }
    public int NumberOfLives { get; set; }
}

public class DogInput
{
    public string Name { get; set; }
    public bool WagsTail { get; set; }
}

[MutationType]
public static partial class PetMutations
{
    public static Pet CreatePet(PetInput input)
    {
        // ...
    }
}
```

</Implementation>
<Code>

```csharp
public class PetInputType : InputObjectType<PetInput>
{
    protected override void Configure(
        IInputObjectTypeDescriptor<PetInput> descriptor)
    {
        descriptor.OneOf();
    }
}
```

</Code>
</ExampleTabs>

All fields on a `@oneOf` input must be nullable. Hot Chocolate validates at runtime that exactly one field is provided.

A `@oneOf` input must also have at least one field that can hold a finite value, that is, a scalar, an enum, a list, or an input object that is itself finite. `#!sdl input A @oneOf { self: A }` is rejected when the schema is built.

# Next Steps

- **Need scalar arguments?** See [Arguments](./arguments.md).
- **Need to write mutations?** See [Mutations](./mutations.md).
- **Need to understand nullability?** See [Non-Null](./non-null.md).
- **Need to document input fields?** See [Documentation](./documentation.md).

---

### Website Content Docs Hotchocolate Defining A Schema Interfaces (`website/content/docs/hotchocolate/defining-a-schema/interfaces.md`)

---
title: "Interfaces"
description: "Define GraphQL interfaces in Hot Chocolate with the [InterfaceType] attribute, sharing fields across object types and mapping them from C# interfaces."
---

A GraphQL interface defines a set of fields that multiple object types share. When a field returns an interface type, the client can query the shared fields directly and use fragments to access type-specific fields. Interfaces are output-only types and cannot be used as arguments or input fields.

**GraphQL schema**

```graphql
interface Message {
  author: User!
  createdAt: DateTime!
}

type TextMessage implements Message {
  author: User!
  createdAt: DateTime!
  content: String!
}

type Query {
  messages: [Message]!
}
```

**Client query**

```graphql
{
  messages {
    createdAt
    ... on TextMessage {
      content
    }
  }
}
```

The shared `createdAt` field is queried directly on the interface. The `content` field, which exists only on `TextMessage`, is accessed through an inline fragment.

# Defining an Interface Type

Hot Chocolate maps C# interfaces and abstract classes to GraphQL interface types.

<ExampleTabs>
<Implementation>

```csharp
[InterfaceType("Message")]
public interface IMessage
{
    User Author { get; set; }
    DateTime CreatedAt { get; set; }
}

public class TextMessage : IMessage
{
    public User Author { get; set; }
    public DateTime CreatedAt { get; set; }
    public string Content { get; set; }
}

[QueryType]
public static partial class MessageQueries
{
    public static IMessage[] GetMessages()
    {
        // ...
    }
}
```

```csharp
builder
    .AddGraphQL()
    .AddType<TextMessage>();
```

You must register each implementing type explicitly so Hot Chocolate knows which object types belong to the interface.

You can also use an abstract class instead of an interface:

```csharp
[InterfaceType]
public abstract class Message
{
    public User Author { get; set; }
    public DateTime CreatedAt { get; set; }
}
```

</Implementation>
<Code>

```csharp
public interface IMessage
{
    User Author { get; set; }
    DateTime CreatedAt { get; set; }
}

public class MessageType : InterfaceType<IMessage>
{
    protected override void Configure(
        IInterfaceTypeDescriptor<IMessage> descriptor)
    {
        descriptor.Name("Message");
    }
}

public class TextMessage : IMessage
{
    public User Author { get; set; }
    public DateTime CreatedAt { get; set; }
    public string Content { get; set; }
}

public class TextMessageType : ObjectType<TextMessage>
{
    protected override void Configure(
        IObjectTypeDescriptor<TextMessage> descriptor)
    {
        descriptor.Implements<MessageType>();
    }
}
```

```csharp
builder
    .AddGraphQL()
    .AddQueryType<QueryType>()
    .AddType<TextMessageType>();
```

</Code>
</ExampleTabs>

# Ignoring Fields

You can exclude specific fields from the GraphQL interface.

<ExampleTabs>
<Implementation>

```csharp
[InterfaceType("Message")]
public interface IMessage
{
    [GraphQLIgnore]
    User Author { get; set; }

    DateTime CreatedAt { get; set; }
}
```

</Implementation>
<Code>

```csharp
public class MessageType : InterfaceType<IMessage>
{
    protected override void Configure(
        IInterfaceTypeDescriptor<IMessage> descriptor)
    {
        descriptor.Ignore(f => f.Author);
    }
}
```

</Code>
</ExampleTabs>

# Overriding Names

Use `[GraphQLName]` or the `Name` method to override inferred names.

<ExampleTabs>
<Implementation>

```csharp
[GraphQLName("Post")]
public interface IMessage
{
    User Author { get; set; }

    [GraphQLName("addedAt")]
    DateTime CreatedAt { get; set; }
}
```

You can also specify the name through the `[InterfaceType]` attribute:

```csharp
[InterfaceType("Post")]
public interface IMessage
```

</Implementation>
<Code>

```csharp
public class MessageType : InterfaceType<IMessage>
{
    protected override void Configure(
        IInterfaceTypeDescriptor<IMessage> descriptor)
    {
        descriptor.Name("Post");

        descriptor
            .Field(f => f.CreatedAt)
            .Name("addedAt");
    }
}
```

</Code>
</ExampleTabs>

Both produce the following schema:

```graphql
interface Post {
  author: User!
  addedAt: DateTime!
}
```

# Interfaces Implementing Interfaces

GraphQL interfaces can implement other interfaces, forming a hierarchy.

<ExampleTabs>
<Implementation>

```csharp
[InterfaceType("Message")]
public interface IMessage
{
    User Author { get; set; }
}

[InterfaceType("DatedMessage")]
public interface IDatedMessage : IMessage
{
    DateTime CreatedAt { get; set; }
}

public class TextMessage : IDatedMessage
{
    public User Author { get; set; }
    public DateTime CreatedAt { get; set; }
    public string Content { get; set; }
}
```

```csharp
builder
    .AddGraphQL()
    .AddType<IDatedMessage>()
    .AddType<TextMessage>();
```

</Implementation>
<Code>

```csharp
public class DatedMessageType : InterfaceType<IDatedMessage>
{
    protected override void Configure(
        IInterfaceTypeDescriptor<IDatedMessage> descriptor)
    {
        descriptor.Name("DatedMessage");
        descriptor.Implements<MessageType>();
    }
}

public class TextMessageType : ObjectType<TextMessage>
{
    protected override void Configure(
        IObjectTypeDescriptor<TextMessage> descriptor)
    {
        descriptor.Implements<DatedMessageType>();
    }
}
```

```csharp
builder
    .AddGraphQL()
    .AddQueryType<QueryType>()
    .AddType<DatedMessageType>()
    .AddType<TextMessageType>();
```

</Code>
</ExampleTabs>

Register intermediate interfaces (like `DatedMessage`) explicitly if they are not returned directly from a resolver field.

# Default Resolvers

Interface fields can define default resolvers, similar to default interface methods in C#. When an object type implements the interface, it automatically inherits the resolver for any field where it does not define its own. If the object type does not even declare the field, the field is created automatically with the interface's resolver.

This is useful when multiple types share the same resolution logic for a field and you want to define it once on the interface rather than repeating it in every implementing type.

<ExampleTabs>
<Implementation>

```csharp
[InterfaceType("Message")]
public interface IMessage
{
    User Author { get; }
    DateTime CreatedAt { get; }
}

[InterfaceType<IMessage>]
public static partial class MessageNode
{
    public static string DisplayName([Parent] IMessage message)
        => $"{message.Author.Name} - {message.CreatedAt:d}";
}
```

All object types implementing `IMessage` inherit the `displayName` field and its resolver. No additional configuration is needed on the implementing types.

</Implementation>
<Code>

```csharp
public class MessageType : InterfaceType
{
    protected override void Configure(IInterfaceTypeDescriptor descriptor)
    {
        descriptor.Name("Message");

        descriptor
            .Field("displayName")
            .Type<StringType>()
            .Resolve(context =>
            {
                var message = context.Parent<IMessage>();
                return $"{message.Author.Name} - {message.CreatedAt:d}";
            });
    }
}

public class TextMessageType : ObjectType<TextMessage>
{
    protected override void Configure(
        IObjectTypeDescriptor<TextMessage> descriptor)
    {
        descriptor.Implements<MessageType>();
    }
}
```

`TextMessageType` inherits the `displayName` field and its resolver from `MessageType`. No additional configuration is needed.

</Code>
</ExampleTabs>

Object types can override an inherited resolver by defining their own resolver for the same field.

# Next Steps

- **Need types without shared fields?** See [Unions](./unions.md).
- **Need to define output types?** See [Object Types](./object-types.md).
- **Need to extend an existing type?** See [Extending Types](./object-types.md).
- **Need to document interface fields?** See [Documentation](./documentation.md).

---

### Website Content Docs Hotchocolate Defining A Schema Lists (`website/content/docs/hotchocolate/defining-a-schema/lists.md`)

---
title: "Lists"
description: "Expose .NET collections as GraphQL list types in Hot Chocolate, covering supported collection types, list nullability, and nested list definitions."
---

GraphQL lists represent ordered collections of elements. When a resolver returns any .NET collection type, Hot Chocolate exposes it as a list in the schema.

**GraphQL schema**

```graphql
type Query {
  users: [User!]!
}
```

**Client query**

```graphql
{
  users {
    id
    name
  }
}
```

The response contains an ordered array of objects matching the requested fields.

# Supported Collection Types

Hot Chocolate recognizes common .NET collection types and maps them to GraphQL lists.

| C# return type        | GraphQL type (NRT enabled) |
| --------------------- | -------------------------- |
| `List<User>`          | `[User!]!`                 |
| `User[]`              | `[User!]!`                 |
| `IEnumerable<User>`   | `[User!]!`                 |
| `IReadOnlyList<User>` | `[User!]!`                 |
| `IQueryable<User>`    | `[User!]!`                 |
| `List<User?>`         | `[User]!`                  |
| `List<User>?`         | `[User!]`                  |

Any type implementing `IEnumerable<T>` is treated as a list.

# Defining List Fields

<ExampleTabs>
<Implementation>

```csharp
[QueryType]
public static partial class UserQueries
{
    public static List<User> GetUsers(CatalogContext db)
        => db.Users.ToList();
}
```

The return type `List<User>` is automatically mapped to `[User!]!` when NRT is enabled.

</Implementation>
<Code>

```csharp
public class UserQueriesType : ObjectType
{
    protected override void Configure(IObjectTypeDescriptor descriptor)
    {
        descriptor.Name(OperationTypeNames.Query);

        descriptor
            .Field("users")
            .Type<ListType<UserType>>()
            .Resolve(context =>
            {
                // ...
            });
    }
}
```

Using `ListType<T>` makes the list type explicit in the descriptor.

</Code>
</ExampleTabs>

# List Nullability

Lists have two layers of nullability: the list itself and its items. With [nullable reference types](./non-null.md) enabled, Hot Chocolate infers both layers from your C# types.

| C# type          | GraphQL type | Meaning                                     |
| ---------------- | ------------ | ------------------------------------------- |
| `List<string>`   | `[String!]!` | Non-null list of non-null items             |
| `List<string?>`  | `[String]!`  | Non-null list, items can be null            |
| `List<string>?`  | `[String!]`  | List itself can be null, items are non-null |
| `List<string?>?` | `[String]`   | Both list and items can be null             |

If you need to override the inferred nullability, use `[GraphQLType]` or the descriptor API:

```csharp
// Override to allow null items
[GraphQLType(typeof(ListType<StringType>))]
public List<string> Tags { get; set; }
```

# Nested Lists

Hot Chocolate supports nested lists (lists of lists). This pattern is useful for representing matrix-like data.

```csharp
[QueryType]
public static partial class GridQueries
{
    public static List<List<int>> GetMatrix()
        => [[1, 2], [3, 4]];
}
```

This produces `matrix: [[Int!]!]!` in the schema.

# Next Steps

- **Need to control nullability?** See [Non-Null](./non-null.md).
- **Need pagination instead of full lists?** See [Pagination](../fetching-data/pagination.md).
- **Need to filter or sort lists?** See [Filtering](../fetching-data/filtering.md) and [Sorting](../fetching-data/sorting.md).

---

### Website Content Docs Hotchocolate Defining A Schema Mutations (`website/content/docs/hotchocolate/defining-a-schema/mutations.md`)

---
title: "Mutations"
description: "Define GraphQL mutations in Hot Chocolate with the [MutationType] attribute and enable mutation conventions for inputs, payloads, and typed errors."
---

The Mutation type is the entry point for write operations. Unlike query fields, mutation fields are expected to cause side effects: creating, updating, or deleting data. GraphQL executes top-level mutation fields serially, one after another, to guarantee ordering. Child fields of a mutation result are executed in parallel, like any other object type.

**GraphQL schema**

```graphql
type Mutation {
  addBook(input: AddBookInput!): AddBookPayload!
  publishBook(input: PublishBookInput!): PublishBookPayload!
}
```

**Client mutation**

```graphql
mutation {
  addBook(input: { title: "C# in depth" }) {
    book {
      id
      title
    }
  }
}
```

# Defining a Mutation Type

Mark a class with `[MutationType]` and the source generator registers it as part of the Mutation type. Like query types, the class must be `partial`.

<ExampleTabs>
<Implementation>

```csharp
[MutationType]
public static partial class BookMutations
{
    public static async Task<Book> AddBookAsync(
        string title,
        string author,
        CatalogContext db,
        CancellationToken ct)
    {
        var book = new Book { Title = title, Author = author };
        db.Books.Add(book);
        await db.SaveChangesAsync(ct);
        return book;
    }
}
```

</Implementation>
<Code>

```csharp
public class BookMutations
{
    public async Task<Book> AddBookAsync(
        string title,
        string author,
        CatalogContext db,
        CancellationToken ct)
    {
        var book = new Book { Title = title, Author = author };
        db.Books.Add(book);
        await db.SaveChangesAsync(ct);
        return book;
    }
}

public class BookMutationsType : ObjectType<BookMutations>
{
    protected override void Configure(
        IObjectTypeDescriptor<BookMutations> descriptor)
    {
        descriptor.Field(f => f.AddBookAsync(default!, default!, default!, default!));
    }
}
```

```csharp
builder
    .AddGraphQL()
    .AddMutationType<BookMutationsType>();
```

</Code>
</ExampleTabs>

# Splitting Across Classes

Like query types, you can annotate multiple classes with `[MutationType]`. The source generator merges them into one Mutation type.

```csharp
[MutationType]
public static partial class BookMutations
{
    public static async Task<Book> AddBookAsync(
        string title, CatalogContext db, CancellationToken ct)
    {
        var book = new Book { Title = title };
        db.Books.Add(book);
        await db.SaveChangesAsync(ct);
        return book;
    }
}
```

```csharp
[MutationType]
public static partial class AuthorMutations
{
    public static async Task<Author> AddAuthorAsync(
        string name, CatalogContext db, CancellationToken ct)
    {
        var author = new Author { Name = name };
        db.Authors.Add(author);
        await db.SaveChangesAsync(ct);
        return author;
    }
}
```

# Mutation Conventions

In GraphQL, it is best practice for each mutation to accept a single `input` argument and return a payload object. The payload contains the changed data and any domain errors. This pattern keeps the schema evolvable but requires boilerplate.

Hot Chocolate generates the input and payload types for you when mutation conventions are enabled.

```csharp
builder
    .AddGraphQL()
    .AddMutationConventions(applyToAllMutations: true);
```

With conventions enabled, you write the resolver with plain parameters and a return type. Hot Chocolate wraps them in an input type and payload type automatically.

<ExampleTabs>
<Implementation>

```csharp
[MutationType]
public static partial class UserMutations
{
    public static async Task<User?> UpdateUserNameAsync(
        [ID] Guid userId,
        string username,
        UserService users,
        CancellationToken ct)
        => await users.UpdateNameAsync(userId, username, ct);
}
```

</Implementation>
<Code>

```csharp
public class UserMutationsType : ObjectType<UserMutations>
{
    protected override void Configure(
        IObjectTypeDescriptor<UserMutations> descriptor)
    {
        descriptor
            .Field(f => f.UpdateUserNameAsync(default, default!, default!, default))
            .Argument("userId", a => a.ID());
    }
}
```

</Code>
</ExampleTabs>

This produces the following schema:

```graphql
type Mutation {
  updateUserName(input: UpdateUserNameInput!): UpdateUserNamePayload!
}

input UpdateUserNameInput {
  userId: ID!
  username: String!
}

type UpdateUserNamePayload {
  user: User
}
```

Services (`UserService`, `CancellationToken`) are not included in the generated input type. Only parameters that map to GraphQL arguments appear in the input.

If you prefer to opt in per mutation instead of globally, use `[UseMutationConvention]` on individual methods:

```csharp
[UseMutationConvention]
public static async Task<User?> UpdateUserNameAsync(/* ... */)
```

## Opting Out

To exclude a specific mutation from global conventions:

```csharp
[UseMutationConvention(Disable = true)]
public static async Task<User?> UpdateUserNameAsync(/* ... */)
```

You can also partially opt out by providing your own input or payload type. If your method already accepts a type named `{MutationName}Input` or returns `{MutationName}Payload`, the convention recognizes it and does not generate a replacement.

```csharp
public static UpdateUserNamePayload UpdateUserNameAsync(UpdateUserNameInput input)
{
    // Custom payload and input — conventions leave them as-is
}
```

## Customizing Names

Override the global naming patterns through `MutationConventionOptions`:

```csharp
builder
    .AddGraphQL()
    .AddMutationConventions(
        new MutationConventionOptions
        {
            InputArgumentName = "input",
            InputTypeNamePattern = "{MutationName}Input",
            PayloadTypeNamePattern = "{MutationName}Payload",
            PayloadErrorTypeNamePattern = "{MutationName}Error",
            PayloadErrorsFieldName = "errors",
            ApplyToAllMutations = true
        });
```

Override per mutation with `[UseMutationConvention]`:

```csharp
[UseMutationConvention(
    InputTypeName = "RenameUserInput",
    PayloadTypeName = "RenameUserPayload")]
public static async Task<User?> UpdateUserNameAsync(/* ... */)
```

# Domain Errors

Mutation conventions support typed domain errors on the payload. Annotate a mutation with `[Error]` to declare which exceptions represent domain errors. Hot Chocolate catches those exceptions and maps them to error types on the payload. All other exceptions remain runtime errors.

```csharp
[MutationType]
public static partial class UserMutations
{
    [Error(typeof(UserNameTakenException))]
    [Error(typeof(InvalidUserNameException))]
    public static async Task<User?> UpdateUserNameAsync(
        [ID] Guid userId,
        string username,
        UserService users,
        CancellationToken ct)
        => await users.UpdateNameAsync(userId, username, ct);
}
```

This produces a payload with an `errors` field:

```graphql
type UpdateUserNamePayload {
  user: User
  errors: [UpdateUserNameError!]
}

interface Error {
  message: String!
}

type UserNameTakenError implements Error {
  message: String!
}

type InvalidUserNameError implements Error {
  message: String!
}

union UpdateUserNameError = UserNameTakenError | InvalidUserNameError
```

Exception class names are rewritten: `UserNameTakenException` becomes `UserNameTakenError` in the schema.

## Controlling Error Shape

There are three ways to map an exception to a schema error:

**Map the exception directly.** Annotate `[Error(typeof(MyException))]`. The exception's `Message` property becomes the error message. This is the quickest approach.

**Map with a factory method.** Create an error class with a `#!csharp public static CreateErrorFrom(MyException ex)` method. This lets you control the error shape and hide internal details.

```csharp
public class UserNameTakenError
{
    private UserNameTakenError(string message) => Message = message;

    public string Message { get; }

    public static UserNameTakenError CreateErrorFrom(UserNameTakenException ex)
        => new($"The username {ex.Username} is already taken.");
}
```

**Map with a constructor.** Give the error class a constructor that accepts the exception.

```csharp
public class UserNameTakenError
{
    public UserNameTakenError(UserNameTakenException ex)
        => Message = $"The username {ex.Username} is already taken.";

    public string Message { get; }
}
```

Factory methods can also be instance methods implementing `IPayloadErrorFactory<TError, TException>`, which supports dependency injection. Errors and error factories can be shared across multiple mutations. You can use `AggregateException` to return multiple errors at once.

## Custom Error Interface

The default error interface requires a `message` field. To add an error code or other fields, define your own interface:

<ExampleTabs>
<Implementation>

```csharp
[GraphQLName("UserError")]
public interface IUserError
{
    string Message { get; }
    string Code { get; }
}
```

```csharp
builder
    .AddGraphQL()
    .AddErrorInterfaceType<IUserError>();
```

</Implementation>
<Code>

```csharp
public class CustomErrorInterfaceType : InterfaceType
{
    protected override void Configure(IInterfaceTypeDescriptor descriptor)
    {
        descriptor.Name("UserError");
        descriptor.Field("message").Type<NonNullType<StringType>>();
        descriptor.Field("code").Type<NonNullType<StringType>>();
    }
}
```

```csharp
builder
    .AddGraphQL()
    .AddErrorInterfaceType<CustomErrorInterfaceType>();
```

</Code>
</ExampleTabs>

All error types must declare the fields required by the interface. They do not need to implement the C# interface, but they must have matching properties.

# Next Steps

- **Need to read data?** See [Queries](./queries.md).
- **Need real-time updates?** See [Subscriptions](./subscriptions.md).
- **Need to understand input types?** See [Input Object Types](./input-object-types.md).
- **Need to fetch data efficiently?** See [DataLoader](../fetching-data/batching/dataloader.md).

---

### Website Content Docs Hotchocolate Defining A Schema Non Null (`website/content/docs/hotchocolate/defining-a-schema/non-null.md`)

---
title: "Non-Null"
description: "How Hot Chocolate maps C# nullable reference types to the GraphQL non-null modifier, with explicit overrides like [GraphQLNonNullType] and non-null list items."
---

By default, every GraphQL field can return either its declared type or `null`. The non-null modifier (`!`) tells clients that a field will never be `null`. If a resolver returns `null` for a non-null field, the execution engine raises an error rather than sending unexpected null values to clients.

```graphql
type User {
  name: String!
  bio: String
}
```

In this schema, `name` always has a value. The `bio` field may be `null`.

# Implicit Nullability from C# Types

Hot Chocolate infers nullability from your C# types. When [nullable reference types](https://docs.microsoft.com/dotnet/csharp/nullable-references) (NRT) are enabled in your project, the mapping is straightforward.

## Value Types

Value types are non-null by default. Use `?` to make them nullable.

| C# type | GraphQL type |
| ------- | ------------ |
| `int`   | `Int!`       |
| `int?`  | `Int`        |
| `bool`  | `Boolean!`   |
| `bool?` | `Boolean`    |

## Reference Types (NRT Enabled)

With NRT enabled (recommended), non-nullable references map to non-null GraphQL types.

| C# type   | GraphQL type |
| --------- | ------------ |
| `string`  | `String!`    |
| `string?` | `String`     |
| `User`    | `User!`      |
| `User?`   | `User`       |

## Reference Types (NRT Disabled)

Without NRT, all reference types are nullable by default. Hot Chocolate cannot distinguish `string` from `string?` because the compiler treats them identically.

| C# type  | GraphQL type |
| -------- | ------------ |
| `string` | `String`     |
| `User`   | `User`       |

We strongly recommend enabling NRT. It provides accurate schema nullability without extra attributes and catches null-related bugs at compile time.

# Enabling Nullable Reference Types

Add the following to your `.csproj` file to enable NRT across the project:

```xml
<PropertyGroup>
    <Nullable>enable</Nullable>
</PropertyGroup>
```

You can also enable it per file with `#nullable enable` at the top of the file.

# Explicit Nullability

When you need to override the inferred nullability, use attributes or the descriptor API.

<ExampleTabs>
<Implementation>

```csharp
public class Book
{
    [GraphQLNonNullType]
    public string Title { get; set; }

    public string? Author { get; set; }
}
```

`[GraphQLNonNullType]` forces the field to be non-null in the schema regardless of the C# nullability.

</Implementation>
<Code>

```csharp
public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor
            .Field(f => f.Title)
            .Type<NonNullType<StringType>>();

        descriptor
            .Field(f => f.Author)
            .Type<StringType>();
    }
}
```

</Code>
</ExampleTabs>

# Non-Null List Items

Lists have two layers of nullability: the list itself and its items. With NRT enabled:

| C# type          | GraphQL type |
| ---------------- | ------------ |
| `List<string>`   | `[String!]!` |
| `List<string>?`  | `[String!]`  |
| `List<string?>`  | `[String]!`  |
| `List<string?>?` | `[String]`   |

To override nullability on list items explicitly:

<ExampleTabs>
<Implementation>

```csharp
public class Book
{
    [GraphQLType(typeof(ListType<NonNullType<StringType>>))]
    public List<string> Genres { get; set; }
}
```

</Implementation>
<Code>

```csharp
public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor
            .Field(f => f.Genres)
            .Type<ListType<NonNullType<StringType>>>();
    }
}
```

</Code>
</ExampleTabs>

Both produce `genres: [String!]` in the schema.

# Next Steps

- **Need to define lists?** See [Lists](./lists.md).
- **Need to understand arguments?** See [Arguments](./arguments.md).
- **Need input types?** See [Input Object Types](./input-object-types.md).
- **Need to learn about scalars?** See [Scalars](./scalars.md).

---

### Website Content Docs Hotchocolate Defining A Schema Object Types (`website/content/docs/hotchocolate/defining-a-schema/object-types.md`)

---
title: "Object Types"
metaTitle: "GraphQL Object Types in Hot Chocolate"
description: "Define GraphQL object types in Hot Chocolate with the [ObjectType<T>] attribute, adding fields, resolvers, and type extensions to build your schema graph."
---

Object types are the building blocks of a GraphQL schema. Each object type has a name and a set of fields. Fields can return scalars like `String` and `Int`, or other object types, forming a graph that clients traverse through their queries.

```graphql
type Product {
  name: String!
  price: Decimal!
  inStock: Boolean!
}

type Author {
  name: String!
  bio: String
  books: [Book!]!
}

type Book {
  title: String!
  author: Author!
}
```

Every field in a query resolves to a concrete value. Object types define the shape of that value. Understanding how to define and configure them is the foundation of building a Hot Chocolate schema.

# Defining Object Types

In the implementation-first approach, a C# class becomes a GraphQL object type automatically. The source generator picks up public properties and methods and maps them to fields. In the code-first approach, you create a class that inherits from `ObjectType<T>` and configure it explicitly.

<ExampleTabs>
<Implementation>

```csharp
public class Author
{
    public string Name { get; set; }

    public string? Bio { get; set; }
}
```

Public properties become fields on the `Author` object type. No additional registration or configuration is required beyond the standard `AddTypes` call generated by the source generator.

</Implementation>
<Code>

```csharp
public class Author
{
    public string Name { get; set; }

    public string? Bio { get; set; }
}

public class AuthorType : ObjectType<Author>
{
}
```

When you inherit from `ObjectType<Author>`, the public properties on `Author` are mapped to fields automatically.

You can override the `Configure` method to customize the type further.

```csharp
public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {
        descriptor
            .Field(f => f.Name)
            .Description("The author's full name.");
    }
}
```

Since multiple classes could inherit from `ObjectType<Author>` with different configurations, Hot Chocolate cannot infer which one to use when you return an `Author` from a resolver. You must register code-first types explicitly.

```csharp
builder
    .AddGraphQL()
    .AddType<AuthorType>();
```

With this registration, every `Author` returned from a resolver uses the `AuthorType` configuration.

</Code>
</ExampleTabs>

# Properties as Fields

Public properties with a getter are automatically mapped to GraphQL fields. Hot Chocolate converts the property name to camelCase for the schema.

<ExampleTabs>
<Implementation>

```csharp
public class Product
{
    public int Id { get; set; }

    public string Name { get; set; }

    public decimal Price { get; set; }

    public bool InStock { get; set; }
}
```

</Implementation>
<Code>

```csharp
public class Product
{
    public int Id { get; set; }

    public string Name { get; set; }

    public decimal Price { get; set; }

    public bool InStock { get; set; }
}

public class ProductType : ObjectType<Product>
{
}
```

</Code>
</ExampleTabs>

This produces the following schema:

```graphql
type Product {
  id: Int!
  name: String!
  price: Decimal!
  inStock: Boolean!
}
```

# Methods as Resolvers

Public methods on a class become resolver fields. This is how you add computed fields or fields that fetch data from external sources. Method parameters that are registered services are injected automatically.

<ExampleTabs>
<Implementation>

You can define resolver methods directly on your C# models, as shown below:

```csharp
public class Author
{
    public string Name { get; set; }

    public async Task<List<Book>> GetBooksAsync(
        BookService bookService,
        CancellationToken ct)
        => await bookService.GetBooksByAuthorAsync(Name, ct);
}
```

Here, the `BookService` parameter is injected from the dependency injection container, and `CancellationToken` is provided by the execution engine. Neither appears as a GraphQL argument.

However, a cleaner approach is to separate GraphQL-specific resolvers from your domain models by using a dedicated resolver class. This keeps your domain types free of API concerns and makes them more reusable:

```csharp
public class Author
{
    public string Name { get; set; }
}

[ObjectType<Author>]
public static partial class AuthorNode
{
    public static async Task<List<Book>> GetBooksAsync(
        [Parent] Author author,
        BookService bookService,
        CancellationToken ct)
        => await bookService.GetBooksByAuthorAsync(author.Name, ct);
}
```

The `[ObjectType<Author>]` attribute tells the source generator that this class contributes fields to the `Author` type. The `[Parent]` parameter receives the resolved `Author` instance. The class must be `static partial` so the source generator can wire it up.

</Implementation>
<Code>

```csharp
public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {
        descriptor
            .Field("books")
            .Resolve(async context =>
            {
                var author = context.Parent<Author>();
                var bookService = context.Service<BookService>();
                var ct = context.RequestAborted;
                return await bookService.GetBooksByAuthorAsync(author.Name, ct);
            });
    }
}
```

In the code-first approach, you use the `Resolve` method on the field descriptor to define the resolver logic. The `IResolverContext` provides access to the parent object, services, and the cancellation token.

</Code>
</ExampleTabs>

Both approaches produce this schema:

```graphql
type Author {
  name: String!
  books: [Book!]!
}
```

The naming rules for methods are the same as for [query fields](./queries.md#naming-conventions): `Get` prefixes and `Async` suffixes are stripped, and the result is camelCased.

# Field Configuration

You can rename fields, ignore them, and add descriptions without changing the shape of your C# classes.

## Renaming Fields

<ExampleTabs>
<Implementation>

Use `[GraphQLName]` on a property to change its name in the schema:

```csharp
public class Author
{
    [GraphQLName("fullName")]
    public string Name { get; set; }
}
```

When using a separate resolver class, the method name determines the field name. The `Get` prefix is stripped and the result is camelCased, so `GetFullName` becomes `fullName`:

```csharp
public class Author
{
    public string Name { get; set; }
}

[ObjectType<Author>]
public static partial class AuthorNode
{
    public static string GetFullName([Parent] Author author)
        => author.Name;
}
```

If the naming convention does not produce the name you want, apply `[GraphQLName]` to the resolver method:

```csharp
public class Author
{
    public string Name { get; set; }
}

[ObjectType<Author>]
public static partial class AuthorNode
{
    [GraphQLName("fullName")]
    public static string GetFullName([Parent] Author author)
        => author.Name;
}
```

</Implementation>
<Code>

```csharp
public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {
        descriptor
            .Field(f => f.Name)
            .Name("fullName");
    }
}
```

</Code>
</ExampleTabs>

You can also rename the type itself.

<ExampleTabs>
<Implementation>

Use `[GraphQLName]` on the domain class:

```csharp
[GraphQLName("BookAuthor")]
public class Author
{
    public string Name { get; set; }
}
```

When using a separate resolver class, apply `[GraphQLName]` to the resolver class instead:

```csharp
public class Author
{
    public string Name { get; set; }
}

[ObjectType<Author>]
[GraphQLName("BookAuthor")]
public static partial class AuthorNode
{
}
```

</Implementation>
<Code>

```csharp
public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {
        descriptor.Name("BookAuthor");
    }
}
```

</Code>
</ExampleTabs>

If only one client needs different names, prefer using [aliases](https://graphql.org/learn/queries/#aliases) in that client's queries instead of changing the schema.

## Ignoring Fields

<ExampleTabs>
<Implementation>

Use the `[GraphQLIgnore]` attribute to prevent a property or method from appearing in the schema:

```csharp
public class Product
{
    public string Name { get; set; }

    [GraphQLIgnore]
    public string InternalSku { get; set; }
}
```

For resolver types, you can use the internal fluent API to ignore a field:

```csharp
public class Product
{
    public string Name { get; set; }
    public string InternalSku { get; set; }
}

[ObjectType<Product>]
public static partial class ProductNode
{
    static partial void Configure(IObjectTypeDescriptor<Product> descriptor)
        => descriptor.Ignore(t => t.InternalSku);
}
```

Often, you’ll ignore a field because you want to introduce a resolver that replaces a model property. This can be done with the `[BindMember]` attribute:

```csharp
public class Product
{
    public string Name { get; set; }
    public int BrandId { get; set; }
}

[ObjectType<Product>]
public static partial class ProductNode
{
    [BindMember(nameof(Product.BrandId))]
    public static async Task<Brand> GetBrandAsync(
        [Parent] Product product,
        BrandService brandService,
        CancellationToken cancellationToken)
        => await brandService.GetBrandByIdAsync(product.BrandId, cancellationToken);
}
```

You can also bind multiple members from your model to a single resolver.

</Implementation>
<Code>

Use the `Ignore` method on the descriptor.

```csharp
public class ProductType : ObjectType<Product>
{
    protected override void Configure(IObjectTypeDescriptor<Product> descriptor)
    {
        descriptor.Ignore(f => f.InternalSku);
    }
}
```

</Code>
</ExampleTabs>

## Descriptions

Descriptions appear in GraphQL introspection and tooling like Nitro. They help consumers of your API understand the purpose of each type and field.

<ExampleTabs>
<Implementation>

Use `[GraphQLDescription]` on a class or property:

```csharp
[GraphQLDescription("A product in the catalog.")]
public class Product
{
    [GraphQLDescription("The display name shown to customers.")]
    public string Name { get; set; }

    public decimal Price { get; set; }
}
```

You can also use XML documentation comments. Hot Chocolate reads `<summary>` tags when `UseXmlDocumentation` is enabled (it is enabled by default).

```csharp
/// <summary>
/// A product in the catalog.
/// </summary>
public class Product
{
    /// <summary>
    /// The display name shown to customers.
    /// </summary>
    public string Name { get; set; }

    public decimal Price { get; set; }
}
```

When using a separate resolver class, apply `[GraphQLDescription]` to the resolver class or its methods. XML documentation comments work the same way:

```csharp
public class Product
{
    public string Name { get; set; }

    public decimal Price { get; set; }
}

[ObjectType<Product>]
[GraphQLDescription("A product in the catalog.")]
public static partial class ProductNode
{
    [GraphQLDescription("The display name shown to customers.")]
    public static string GetName([Parent] Product product)
        => product.Name;
}
```

</Implementation>
<Code>

```csharp
public class ProductType : ObjectType<Product>
{
    protected override void Configure(IObjectTypeDescriptor<Product> descriptor)
    {
        descriptor.Description("A product in the catalog.");

        descriptor
            .Field(f => f.Name)
            .Description("The display name shown to customers.");
    }
}
```

</Code>
</ExampleTabs>

## Explicit Binding

By default, all public properties and methods are included as fields. You can switch to explicit binding, where you opt in to each field individually.

```csharp
public class ProductType : ObjectType<Product>
{
    protected override void Configure(IObjectTypeDescriptor<Product> descriptor)
    {
        descriptor.BindFieldsExplicitly();

        descriptor.Field(f => f.Name);
        descriptor.Field(f => f.Price);
    }
}
```

Only `name` and `price` appear in the schema. All other properties on `Product` are excluded.

You can also set this globally, which affects all types.

```csharp
builder
    .AddGraphQL()
    .ModifyOptions(options =>
    {
        options.DefaultBindingBehavior = BindingBehavior.Explicit;
    });
```

# Nullability

Hot Chocolate uses C# nullability to determine whether a GraphQL field is nullable or non-null. When [nullable reference types](https://learn.microsoft.com/dotnet/csharp/nullable-references) are enabled in your project, the mapping is straightforward.

| C# Type         | GraphQL Type |
| --------------- | ------------ |
| `string`        | `String!`    |
| `string?`       | `String`     |
| `int`           | `Int!`       |
| `int?`          | `Int`        |
| `List<string>`  | `[String!]!` |
| `List<string?>` | `[String]!`  |
| `List<string>?` | `[String!]`  |

Value types (`int`, `bool`, `decimal`) are non-null by default. Their nullable counterpart (`int?`, `bool?`) maps to a nullable GraphQL field.

Reference types follow your project's nullable reference type settings. With nullable reference types enabled (recommended), `string` maps to `String!` and `string?` maps to `String`. Without nullable reference types enabled, all reference type fields are nullable by default.

You can override the inferred nullability when needed.

<ExampleTabs>
<Implementation>

```csharp
public class Product
{
    [GraphQLNonNullType]
    public string? Name { get; set; }
}
```

</Implementation>
<Code>

```csharp
public class ProductType : ObjectType<Product>
{
    protected override void Configure(IObjectTypeDescriptor<Product> descriptor)
    {
        descriptor
            .Field(f => f.Name)
            .Type<NonNullType<StringType>>();
    }
}
```

</Code>
</ExampleTabs>

For full details on nullability, see [Non-Null](./non-null.md).

# Dictionary Support

Hot Chocolate automatically maps `Dictionary<TKey, TValue>` properties to a list of key-value pair objects. This eliminates the need for custom resolvers when exposing dictionary data.

<ExampleTabs>
<Implementation>

```csharp
public class Product
{
    public string Name { get; set; }

    public Dictionary<string, string> Attributes { get; set; }
}
```

</Implementation>
<Code>

```csharp
public class Product
{
    public string Name { get; set; }

    public Dictionary<string, string> Attributes { get; set; }
}

public class ProductType : ObjectType<Product>
{
}
```

</Code>
</ExampleTabs>

This produces the following schema:

```graphql
type Product {
  name: String!
  attributes: [KeyValuePairOfStringAndString!]!
}

type KeyValuePairOfStringAndString {
  key: String!
  value: String!
}
```

Clients query dictionary fields like any other list.

```graphql
{
  product {
    name
    attributes {
      key
      value
    }
  }
}
```

This works with any key and value types. For example, `Dictionary<string, int>` produces `KeyValuePairOfStringAndInt32` with the appropriate scalar types.

# Next Steps

- **Need to define query entry points?** See [Queries](./queries.md).
- **Need to understand resolver patterns?** See [Resolvers](../resolvers/index.md).
- **Need to compose types from multiple classes?** See [Extending Types](./object-types.md).
- **Need to define input for mutations?** See [Input Object Types](./input-object-types.md).
- **Need to fetch data efficiently?** See [DataLoader](../fetching-data/batching/dataloader.md).

---

### Website Content Docs Hotchocolate Defining A Schema Queries (`website/content/docs/hotchocolate/defining-a-schema/queries.md`)

---
title: "Queries"
description: "Define the GraphQL Query type in Hot Chocolate with the [QueryType] attribute, splitting root fields across classes and writing side-effect-free resolvers."
---

The Query type is the entry point for reading data from a GraphQL server. It is the only required root type. Every public method or property on a query class becomes a field that clients can request. Query fields are expected to be side-effect-free, which allows the execution engine to run them in parallel.

**GraphQL schema**

```graphql
type Query {
  books: [Book!]!
  author(id: Int!): Author
}
```

**Client query**

```graphql
query {
  books {
    title
  }
  author(id: 1) {
    name
  }
}
```

Both `books` and `author` run concurrently. The execution engine is free to parallelize and reorder query fields because they have no side effects.

# Defining a Query Type

Mark a class with `[QueryType]` and the source generator registers it as part of the Query type. The class must be `partial` so the source generator can add code at build time.

<ExampleTabs>
<Implementation>

```csharp
[QueryType]
public static partial class BookQueries
{
    public static Book GetBook()
        => new Book { Title = "C# in depth", Author = "Jon Skeet" };
}
```

The source generator creates a `book` field on the Query type. No additional registration is needed beyond the `AddTypes` call generated by the source generator.

</Implementation>
<Code>

```csharp
public class BookQueries
{
    public Book GetBook()
        => new Book { Title = "C# in depth", Author = "Jon Skeet" };
}

public class BookQueriesType : ObjectType<BookQueries>
{
    protected override void Configure(IObjectTypeDescriptor<BookQueries> descriptor)
    {
        descriptor
            .Field(f => f.GetBook())
            .Type<BookType>();
    }
}
```

```csharp
builder
    .AddGraphQL()
    .AddQueryType<BookQueriesType>();
```

</Code>
</ExampleTabs>

# Splitting the Query Type Across Classes

GraphQL allows only one Query type per schema, but your codebase does not have to define all query fields in a single class. With the source generator, you can annotate multiple classes with `[QueryType]`. The source generator merges them into one Query type.

```csharp
[QueryType]
public static partial class ProductQueries
{
    public static async Task<Product?> GetProductByIdAsync(
        int id,
        CatalogContext db,
        CancellationToken ct)
        => await db.Products.FindAsync([id], ct);
}
```

```csharp
[QueryType]
public static partial class AuthorQueries
{
    public static async Task<Author?> GetAuthorByIdAsync(
        int id,
        CatalogContext db,
        CancellationToken ct)
        => await db.Authors.FindAsync([id], ct);
}
```

This produces a schema with both fields on the Query type:

```graphql
type Query {
  productById(id: Int!): Product
  authorById(id: Int!): Author
}
```

Group your query classes by domain area. This keeps each file focused and makes it clear which team or module owns each part of the API.

These query classes can also be split across multiple assemblies, as long as each assembly uses the Hot Chocolate source generator.

# Static vs Instance Classes

A `[QueryType]` class can be either static or non-static.

**Static classes** are the recommended default. They have no instance state, which makes resolvers predictable and testable.

```csharp
[QueryType]
public static partial class ProductQueries
{
    public static Product? GetProductById(int id, CatalogContext db)
        => db.Products.Find(id);
}
```

**Non-static classes** are registered as singletons on the service collection by the source generator. Use this when you need constructor-injected dependencies, though injecting services directly into resolver method parameters is preferred in most cases.

```csharp
[QueryType]
public partial class ProductQueries
{
    public Product? GetProductById(int id, CatalogContext db)
        => db.Products.Find(id);
}
```

# Naming Conventions

Hot Chocolate converts C# method names to GraphQL field names using these rules:

| C# Method            | GraphQL Field | Rule                                     |
| -------------------- | ------------- | ---------------------------------------- |
| `GetBook()`          | `book`        | `Get` prefix stripped, camelCased        |
| `GetBookByIdAsync()` | `bookById`    | `Get` prefix and `Async` suffix stripped |
| `Books()`            | `books`       | camelCased as-is                         |

You can override the generated name with `[GraphQLName]`:

```csharp
[GraphQLName("allBooks")]
public static List<Book> GetBooks() => /* ... */;
```

# Next Steps

- **Need to write data?** See [Mutations](./mutations.md).
- **Need real-time updates?** See [Subscriptions](./subscriptions.md).
- **Need to understand how types map to the schema?** See [Object Types](./object-types.md).
- **Need to fetch data efficiently?** See [Resolvers](../resolvers/index.md) and [DataLoader](../fetching-data/batching/dataloader.md).

---

--- METRICS ---
- Files Extracted: 101
- Estimated Token Budget: ~340143 tokens
- Canonical Reference: https://codewiki.google/github.com/ChilliCream/graphql-platform
