From 9714f33948e72be5878043e3e865d99be26729e4 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 10 Mar 2026 12:37:40 +0100 Subject: [PATCH 1/2] [Fusion] Add Field Ownership and Sharing chapter --- website/src/docs/docs.json | 10 +- .../v16/attribute-and-directive-reference.md | 44 +++--- .../v16/data-requirements-and-mapping.md | 6 +- .../docs/fusion/v16/entities-and-lookups.md | 4 +- .../fusion/v16/field-ownership-and-sharing.md | 128 ++++++++++++++++++ 5 files changed, 160 insertions(+), 32 deletions(-) create mode 100644 website/src/docs/fusion/v16/field-ownership-and-sharing.md diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index c230605ad24..8fbadf4b6ca 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -190,7 +190,11 @@ }, { "path": "data-requirements-and-mapping", - "title": "Data Requirements and Mapping" + "title": "Data Requirements" + }, + { + "path": "field-ownership-and-sharing", + "title": "Field Ownership and Sharing" }, { "path": "composition", @@ -204,10 +208,6 @@ "path": "deployment-and-ci-cd", "title": "Deployment and CI/CD" }, - { - "path": "nitro-cli-reference", - "title": "Nitro CLI Reference" - }, { "path": "attribute-and-directive-reference", "title": "Attribute and Directive Reference" diff --git a/website/src/docs/fusion/v16/attribute-and-directive-reference.md b/website/src/docs/fusion/v16/attribute-and-directive-reference.md index d54eec8ef9d..c1c1cedfd5c 100644 --- a/website/src/docs/fusion/v16/attribute-and-directive-reference.md +++ b/website/src/docs/fusion/v16/attribute-and-directive-reference.md @@ -6,28 +6,28 @@ Quick reference for all Fusion-related attributes and their GraphQL directive eq # Attribute and Directive Reference Table -| Attribute | Directive | Description | Guide Page | -| --------------------------- | --------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| `[ObjectType]` | — | Maps static class as extension to entity type T | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[QueryType]` | — | Marks class as contributing Query root fields | [Getting Started](/docs/fusion/v16/getting-started) | -| `[MutationType]` | — | Marks class as contributing Mutation root fields | [Getting Started](/docs/fusion/v16/getting-started) | -| `[SubscriptionType]` | — | Marks class as contributing Subscription root fields | [Getting Started](/docs/fusion/v16/getting-started) | -| `[Lookup]` | `@lookup` | Declares field as entity lookup resolver | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[NodeResolver]` | — | Marks as Relay node resolver | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[Internal]` | `@internal` | Hides lookup from composed schema | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[Shareable]` | `@shareable` | Allows multiple subgraphs to resolve field | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[Parent(requires: "...")]` | — | Declares field requirements from parent | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[Require("...")]` | `@require` | Declares complex field requirements | [Data Requirements and Mapping](/docs/fusion/v16/data-requirements-and-mapping), [Adding a Subgraph](/docs/fusion/v16/adding-a-subgraph) | -| `[EntityKey("...")]` | `@key` | Declares entity key for resolution | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[BindMember(nameof(...))]` | — | Replaces raw FK with resolved entity | [Adding a Subgraph](/docs/fusion/v16/adding-a-subgraph) | -| `[Tag("...")]` | `@tag` | Applies tag for composition filtering | [Composition](/docs/fusion/v16/composition) | -| `[DataLoader]` | — | Source-generates DataLoader interface | [Getting Started](/docs/fusion/v16/getting-started), [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[UsePaging]` | — | Enables cursor-based pagination | [Getting Started](/docs/fusion/v16/getting-started) | -| `[ID]` | — | Declares field as Relay-style ID | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[Inaccessible]` | `@inaccessible` | Hides from composite schema | [Composition](/docs/fusion/v16/composition) | -| `[Override(from: "...")]` | `@override` | Migrates field ownership | [Deployment and CI/CD](/docs/fusion/v16/deployment-and-ci-cd) | -| `[Provides("...")]` | `@provides` | Declares locally-resolvable subfields | [Data Requirements and Mapping](/docs/fusion/v16/data-requirements-and-mapping) | -| `[External]` | `@external` | Field defined by another subgraph | [Data Requirements and Mapping](/docs/fusion/v16/data-requirements-and-mapping) | +| Attribute | Directive | Description | Guide Page | +| --------------------------- | --------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `[ObjectType]` | — | Maps static class as extension to entity type T | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[QueryType]` | — | Marks class as contributing Query root fields | [Getting Started](/docs/fusion/v16/getting-started) | +| `[MutationType]` | — | Marks class as contributing Mutation root fields | [Getting Started](/docs/fusion/v16/getting-started) | +| `[SubscriptionType]` | — | Marks class as contributing Subscription root fields | [Getting Started](/docs/fusion/v16/getting-started) | +| `[Lookup]` | `@lookup` | Declares field as entity lookup resolver | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[NodeResolver]` | — | Marks as Relay node resolver | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[Internal]` | `@internal` | Hides lookup from composed schema | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[Shareable]` | `@shareable` | Allows multiple subgraphs to resolve field | [Field Ownership and Sharing](/docs/fusion/v16/field-ownership-and-sharing) | +| `[Parent(requires: "...")]` | — | Declares field requirements from parent | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[Require("...")]` | `@require` | Declares complex field requirements | [Data Requirements](/docs/fusion/v16/data-requirements-and-mapping), [Adding a Subgraph](/docs/fusion/v16/adding-a-subgraph) | +| `[EntityKey("...")]` | `@key` | Declares entity key for resolution | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[BindMember(nameof(...))]` | — | Replaces raw FK with resolved entity | [Adding a Subgraph](/docs/fusion/v16/adding-a-subgraph) | +| `[Tag("...")]` | `@tag` | Applies tag for composition filtering | [Composition](/docs/fusion/v16/composition) | +| `[DataLoader]` | — | Source-generates DataLoader interface | [Getting Started](/docs/fusion/v16/getting-started), [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[UsePaging]` | — | Enables cursor-based pagination | [Getting Started](/docs/fusion/v16/getting-started) | +| `[ID]` | — | Declares field as Relay-style ID | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[Inaccessible]` | `@inaccessible` | Hides from composite schema | [Composition](/docs/fusion/v16/composition) | +| `[Override(from: "...")]` | `@override` | Migrates field ownership | [Deployment and CI/CD](/docs/fusion/v16/deployment-and-ci-cd) | +| `[Provides("...")]` | `@provides` | Declares locally-resolvable subfields | [Field Ownership and Sharing](/docs/fusion/v16/field-ownership-and-sharing), [Data Requirements](/docs/fusion/v16/data-requirements-and-mapping) | +| `[External]` | `@external` | Field defined by another subgraph | [Field Ownership and Sharing](/docs/fusion/v16/field-ownership-and-sharing), [Data Requirements](/docs/fusion/v16/data-requirements-and-mapping) | # See Also diff --git a/website/src/docs/fusion/v16/data-requirements-and-mapping.md b/website/src/docs/fusion/v16/data-requirements-and-mapping.md index 18d9d10d1c0..e5ea44dbaa1 100644 --- a/website/src/docs/fusion/v16/data-requirements-and-mapping.md +++ b/website/src/docs/fusion/v16/data-requirements-and-mapping.md @@ -1,5 +1,5 @@ --- -title: "Data Requirements and Mapping" +title: "Data Requirements" --- In traditional distributed systems, dependencies between services hide beneath the surface. A service assumes another service provides certain fields, responds in a certain shape, or is available at a certain time. These assumptions are invisible: they live in code, not in contracts. You discover them when something breaks in production. A field gets renamed, a service changes its response format, or a new team removes data another team depended on without knowing. @@ -235,7 +235,7 @@ type Product { The path `seller.addresses[countryCode]` means: navigate to `seller.addresses` (a list), then select `countryCode` from each element. If the seller has three addresses with country codes `"US"`, `"DE"`, and `"US"`, the resolver receives `["US", "DE", "US"]` as the `countryCodes` argument. -## `@provides`: Declaring Contextually Available Fields +## Declaring Contextually Available Fields Use `@provides` on a field that returns an entity to tell the gateway that certain subfields of that entity are available when resolved through this specific field. The subgraph does not own those fields globally, but it can provide them in this context. @@ -389,5 +389,5 @@ If a `@require` argument appears in the composite schema when it should not, che ## Next Steps - **Need entity identity and lookup patterns?** See [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) for the full guide to keys, public vs. internal lookups, and composite keys. -- **Need field ownership and merging rules?** See [Composition](/docs/fusion/v16/composition) for how `@shareable`, `@inaccessible`, and composition validation work. +- **Need field ownership contracts and sharing semantics?** See [Field Ownership and Sharing](/docs/fusion/v16/field-ownership-and-sharing). - **Need the directive and attribute quick reference?** See the [Attribute and Directive Reference](/docs/fusion/v16/attribute-and-directive-reference). diff --git a/website/src/docs/fusion/v16/entities-and-lookups.md b/website/src/docs/fusion/v16/entities-and-lookups.md index cd4863928ce..4bb0245151d 100644 --- a/website/src/docs/fusion/v16/entities-and-lookups.md +++ b/website/src/docs/fusion/v16/entities-and-lookups.md @@ -407,7 +407,7 @@ builder ## Next Steps -- **Need field ownership rules?** See [Composition](/docs/fusion/v16/composition) for how field ownership, `@shareable`, and composition validation work. -- **Need argument mapping and cross-subgraph dependencies?** See [Data Requirements and Mapping](/docs/fusion/v16/data-requirements-and-mapping) for `@is`, `@require`, and FieldSelectionMap patterns. +- **Need field ownership and sharing contracts?** See [Field Ownership and Sharing](/docs/fusion/v16/field-ownership-and-sharing). +- **Need argument mapping and cross-subgraph dependencies?** See [Data Requirements](/docs/fusion/v16/data-requirements-and-mapping) 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](/docs/fusion/v16/authentication-and-authorization) for securing your gateway and subgraphs, or [Deployment and CI/CD](/docs/fusion/v16/deployment-and-ci-cd) for setting up independent subgraph deployments. diff --git a/website/src/docs/fusion/v16/field-ownership-and-sharing.md b/website/src/docs/fusion/v16/field-ownership-and-sharing.md new file mode 100644 index 00000000000..289aca7c85e --- /dev/null +++ b/website/src/docs/fusion/v16/field-ownership-and-sharing.md @@ -0,0 +1,128 @@ +--- +title: "Field Ownership and Sharing" +--- + +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. + +## What `@shareable` Means + +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] +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](/docs/fusion/v16/data-requirements-and-mapping). + +## 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](/docs/fusion/v16/entities-and-lookups). +- **Need dependency and mapping syntax?** See [Data Requirements and Mapping](/docs/fusion/v16/data-requirements-and-mapping). +- **Need merge and validation rules?** See [Composition](/docs/fusion/v16/composition). From fda0d6b02fa155f0edc6bed1dd81ad12b32e4f37 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 10 Mar 2026 20:46:21 +0100 Subject: [PATCH 2/2] edits --- .../src/docs/fusion/v16/adding-a-subgraph.md | 376 ++++++++---------- .../v16/authentication-and-authorization.md | 2 +- website/src/docs/fusion/v16/composition.md | 14 +- .../v16/data-requirements-and-mapping.md | 4 +- .../docs/fusion/v16/deployment-and-ci-cd.md | 4 +- .../fusion/v16/field-ownership-and-sharing.md | 2 +- .../src/docs/fusion/v16/getting-started.md | 2 +- .../coming-from-apollo-federation.md | 24 +- .../migrating-from-schema-stitching.md | 2 +- 9 files changed, 201 insertions(+), 229 deletions(-) diff --git a/website/src/docs/fusion/v16/adding-a-subgraph.md b/website/src/docs/fusion/v16/adding-a-subgraph.md index 0cf29d0263c..a026582d540 100644 --- a/website/src/docs/fusion/v16/adding-a-subgraph.md +++ b/website/src/docs/fusion/v16/adding-a-subgraph.md @@ -2,32 +2,33 @@ title: "Adding a Subgraph" --- -# Adding a Subgraph +You have an existing Fusion project with a gateway, one or more subgraphs, and a working composition pipeline. Now you need to add a new subgraph. Maybe your team owns a new domain (shipping, billing, inventory), or you are splitting an existing subgraph into smaller services. Either way, the process is the same: create a new Hot Chocolate project, define your types and any entity extensions, export the schema, compose, and verify. -You have an existing Fusion project -- a gateway, one or more subgraphs, and a working composition pipeline -- and 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 HotChocolate project, define your types and any entity extensions, export the schema, compose, and verify. - -This page walks 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](/docs/fusion/v16/getting-started) tutorial first. +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](/docs/fusion/v16/getting-started) tutorial first. ## Prerequisites Before you begin, you need: - An existing Fusion project with at least one subgraph and a gateway -- The [Nitro CLI](/docs/fusion/v16/nitro-cli-reference) installed (`dotnet tool install -g ChilliCream.Nitro.CLI`) +- The [Nitro CLI](/docs/fusion/v16/nitro-cli-reference) installed (`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 structure. Here is the canonical layout based on the fusion-demo patterns: +Every Fusion subgraph follows the same layout: ```text src/ Shipping/ Types/ - Product.cs # Entity stubs and type extensions - Query.cs # Query resolvers (lookups) + 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 @@ -35,43 +36,19 @@ src/ schema.graphqls # Exported schema (generated, do not edit) ``` -The `Types/` directory holds your GraphQL type definitions. The `schema.graphqls` and `schema-settings.json` files are generated by schema export -- you edit `schema-settings.json` but never edit `schema.graphqls` directly. - -For projects with data access, you would also have a `Data/` directory for your entity classes, DbContext, and DataLoaders: - -```text -src/ - Shipping/ - Data/ - ShipmentContext.cs # EF Core DbContext - Shipment.cs # Entity class - ShipmentDataLoader.cs # DataLoader for batching - Types/ - ... -``` +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 ASP.NET Core web project and add the required HotChocolate packages: +Create a new GraphQL server project from the template: ```bash -dotnet new web -n Shipping -cd Shipping - -dotnet add package HotChocolate.AspNetCore --version "16.0.0-p.11.2" -dotnet add package HotChocolate.AspNetCore.CommandLine --version "16.0.0-p.11.2" -dotnet add package HotChocolate.Types.Analyzers --version "16.0.0-p.11.2" - -cd .. +dotnet new graphql -n Shipping ``` -The three packages serve different purposes: +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. -- **`HotChocolate.AspNetCore`** -- The GraphQL server -- **`HotChocolate.AspNetCore.CommandLine`** -- Enables `dotnet run -- schema export` for schema generation -- **`HotChocolate.Types.Analyzers`** -- Source generator that auto-registers your types - -Your `Shipping.csproj` should look like this: +Your `Shipping/Shipping.csproj` should look like this: ```xml @@ -82,212 +59,190 @@ Your `Shipping.csproj` should look like this: enable + + + + - - - - all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all ``` -If your project uses shared defaults (like the fusion-demo's `SourceSchemaDefaults` project for shared GraphQL configuration), add a project reference to it as well. Shared defaults keep configuration like `AddGlobalObjectIdentification()`, `AddMutationConventions()`, and `AddInstrumentation()` consistent across all subgraphs. +### Configure the Port -## Configure the Server +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: -Replace the contents of `Program.cs`: +```json +"launchUrl": "http://localhost:5003/graphql", +"applicationUrl": "http://localhost:5003" +``` -```csharp -// Shipping/Program.cs +This ensures the subgraph runs on port 5003, which matches what you will configure in `schema-settings.json` later. -var builder = WebApplication.CreateBuilder(args); +## Define Your Types -builder.Services - .AddGraphQLServer() - .AddTypes(); +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. -var app = builder.Build(); +### Define the Shipment Type -app.MapGraphQL(); -app.RunWithGraphQLCommands(args); -``` +Create `Shipment.cs` in the Shipping project: -This is the same minimal setup as any HotChocolate subgraph. `AddTypes()` is source-generated and registers all types discovered by the analyzer. `RunWithGraphQLCommands(args)` enables CLI commands like schema export. +```csharp +// Shipping/Shipment.cs -If your project has shared defaults, replace `.AddTypes()` with your shared configuration: +namespace Shipping; -```csharp -builder - .AddGraphQL("shipping-api") - .AddDefaultSettings() - .AddShippingTypes(); -``` +public class Shipment +{ + public int Id { get; set; } -The string `"shipping-api"` is the schema name used for schema export naming. + public int ProductId { get; set; } -## Define Your Types + public required string TrackingNumber { get; set; } -The Shipping subgraph adds a `deliveryEstimate` field to the existing `Product` type. It does not own the Product -- the Products subgraph does. The Shipping subgraph extends it. + public required string Status { get; set; } +} +``` -### Create the Entity Stub +Each shipment has a `ProductId` that references a product from the Products subgraph. -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`: +### Add In-Memory Data + +Create `ShipmentRepository.cs`: ```csharp -// Shipping/Types/Product.cs +// Shipping/ShipmentRepository.cs -namespace Shipping.Types; +namespace Shipping; -[EntityKey("id")] -public sealed record Product([property: ID] int Id) +public static class ShipmentRepository { - public int GetDeliveryEstimate( - string zip, - [Require( - """ - { - weight, - length: dimension.length - width: dimension.width - height: dimension.height - } - """)] - ProductDimensionInput dimension) - { - const int baseDays = 2; - var volumeCm3 = dimension.Length * dimension.Width * dimension.Height; - var sizeDays = volumeCm3 > 50000 ? 2 : 0; - var weightDays = dimension.Weight > 500 ? 1 : 0; - return Math.Max(1, baseDays + sizeDays + weightDays); - } + private static readonly List 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 GetByProductId(int productId) + => Shipments.Where(s => s.ProductId == productId).ToList(); } ``` -Several things to notice: - -- **`[EntityKey("id")]`** explicitly declares that `Product` is an entity identified by the `id` field. This is needed here because the Shipping subgraph does not have its own lookup that the composition engine can use to infer the key automatically. When a subgraph has a `[Lookup]` resolver (like `GetProductById(int id)`), the composition engine infers the key from the lookup's arguments. When there is no lookup, use `[EntityKey]` to declare the key explicitly. -- **`record Product(int Id)`** is the entity stub. It only contains the `Id` field -- it does not duplicate `name`, `price`, or any other field from the Products subgraph. -- **`[Require(...)]`** declares that the `deliveryEstimate` resolver needs data from other subgraphs. The `weight` field and `dimension.length`, `dimension.width`, `dimension.height` fields all live in the Products subgraph. The gateway fetches these values automatically and passes them as the `dimension` argument. Clients never see this argument -- it is removed from the composite schema. +### Create the Entity Stub -Create the input type for the required dimension data: +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 -// Shipping/Types/ProductDimensionInput.cs +// Shipping/Types/Product.cs -namespace Shipping.Types; +namespace Shipping; -public sealed class ProductDimensionInput +public sealed record Product(int Id) { - public int Weight { get; init; } - public double Length { get; init; } - public double Width { get; init; } - public double Height { get; init; } + public List GetShipments() + => ShipmentRepository.GetByProductId(Id); } ``` -### Add the Lookup +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. -The gateway needs a way to enter the Shipping subgraph's `Product` type. Create `Types/Query.cs`: +### Add Query Resolvers + +Create `Types/ShippingQueries.cs` with a public lookup for `Shipment` and an internal lookup for `Product`: ```csharp -// Shipping/Types/Query.cs +// Shipping/Types/ShippingQueries.cs -namespace Shipping.Types; +namespace Shipping; [QueryType] -public static partial class Query +public static partial class ShippingQueries { - [Lookup, Internal] - public static Product GetProductById([ID] int id) + [Lookup] + public static Shipment? GetShipmentById(int id) + => ShipmentRepository.GetById(id); + + [Internal, Lookup] + public static Product? GetProductById(int id) => new(id); } ``` -- **`[Lookup]`** marks this as an entity resolution entry point for the gateway. -- **`[Internal]`** hides this lookup from the composite schema. Clients cannot call `productById` on the Shipping subgraph directly -- it exists only for the gateway's internal use during entity resolution. - -The internal lookup constructs a `Product` stub from the ID without checking whether the product exists. This is safe because the gateway only calls internal lookups during entity resolution, after another subgraph has already confirmed the entity exists. +- `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. -### When to Use `[BindMember]` +For more on public vs. internal lookups and when to use each, see [Entities and Lookups](/docs/fusion/v16/entities-and-lookups). -If your subgraph stores a foreign key reference to an entity from another subgraph, use `[BindMember]` to replace the raw ID field with a resolved entity reference. This is common when your subgraph has its own data that references entities from other subgraphs. +### Replace Foreign Keys with Entity References -For example, if the Shipping subgraph had a `Shipment` type with a `productId` field: +The `Shipment` type currently exposes a raw `ProductId`. To expose `shipment.product` instead, add a type extension. Create `Types/ShipmentNode.cs`: ```csharp -// Types/ShipmentNode.cs +// Shipping/Types/ShipmentNode.cs + +using HotChocolate.Types; + +namespace Shipping; [ObjectType] -internal static partial class ShipmentNode +public static partial class ShipmentNode { [BindMember(nameof(Shipment.ProductId))] - public static Product GetProduct( - [Parent(requires: nameof(Shipment.ProductId))] Shipment shipment) + public static Product GetProduct([Parent] Shipment shipment) => new(shipment.ProductId); } ``` -- **`[BindMember(nameof(Shipment.ProductId))]`** tells HotChocolate to replace the `productId` field on `Shipment` with the `product` field returned by this resolver. -- **`[Parent(requires: nameof(Shipment.ProductId))]`** tells the gateway that it needs the `ProductId` from the parent `Shipment` object to resolve this field. +`[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. -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 -The Shipping subgraph in this example does not have its own data, so it does not need `[BindMember]`. But if your new subgraph has entities that reference types from other subgraphs, this pattern is essential. +Set `Program.cs` to: -## Configure `schema-settings.json` +```csharp +var builder = WebApplication.CreateBuilder(args); -When you export the schema for the first time, HotChocolate generates a `schema-settings.json` file alongside the `schema.graphqls` file. This file tells the composition engine about your subgraph. You need to edit it to set the correct values. +builder + .AddGraphQL("Shipping") + .AddTypes(); -Here is the complete format: +var app = builder.Build(); -```json -{ - "name": "shipping-api", - "transports": { - "http": { - "url": "{{API_URL}}", - "clientName": "fusion" - } - }, - "extensions": { - "nitro": { - "apiId": "your-nitro-api-id" - } - }, - "environments": { - "aspire": { - "API_URL": "http://localhost:5003/graphql" - }, - "dev": { - "API_URL": "https://dev.example.com/graphql" - }, - "prod": { - "API_URL": "https://api.example.com/graphql" - } - } -} +app.MapGraphQL(); +app.RunWithGraphQLCommands(args); ``` -### Field Reference +`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. -**`name`** (required) -- The unique identifier for this subgraph in the composed graph. This must be unique across all subgraphs. Convention: use lowercase with hyphens, matching the service name (e.g., `shipping-api`, `products-api`, `accounts-api`). +## Export the Schema -**`transports.http.url`** (required) -- The URL where the gateway can reach this subgraph at runtime. You can use a literal URL like `http://localhost:5003/graphql` or a template variable like `{{API_URL}}` that gets resolved from the `environments` section. +From the project root, export the schema: -**`transports.http.clientName`** (optional) -- The named HTTP client the gateway uses to send requests to this subgraph. Defaults to `"fusion"`. The gateway must register an `HttpClient` with this name (e.g., `builder.Services.AddHttpClient("fusion")`). All subgraphs in the fusion-demo use the same client name `"fusion"`, which means they share a single HTTP client configuration on the gateway. +```bash +dotnet run --project ./Shipping -- schema export +``` -**`extensions.nitro.apiId`** (optional) -- The Nitro cloud API identifier. Only needed if you use Nitro for schema delivery and monitoring. You can omit the entire `extensions` section for local-only development. +This generates two files in the Shipping directory: -**`environments`** (optional) -- Per-environment variable substitutions. Each key is an environment name, and the values are key-value pairs that replace `{{VARIABLE}}` placeholders in the `url` field. When you run `nitro fusion compose --environment aspire`, the CLI resolves `{{API_URL}}` to `http://localhost:5003/graphql`. +- **`schema.graphqls`** contains the subgraph's GraphQL schema. +- **`schema-settings.json`** contains the subgraph settings. -For a simple local setup without environment variables, the minimal `schema-settings.json` is: +Because `Program.cs` uses `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-api", + "name": "Shipping", "transports": { "http": { "url": "http://localhost:5003/graphql" @@ -296,33 +251,18 @@ For a simple local setup without environment variables, the minimal `schema-sett } ``` -## Export the Schema and Compose - -### Export - -From the project root, export the schema: - -```bash -dotnet run ./Shipping -- schema export -``` - -This generates two files in the Shipping directory: - -- `schema.graphqls` -- The subgraph's GraphQL schema -- `schema-settings.json` -- The subgraph settings (edit this as described above) +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. -After export, open `schema-settings.json` and verify or update the `name` and `url` fields. - -### Compose +## Compose Run composition with all subgraph schemas, including your new one: ```bash nitro fusion compose \ - --source-schema-file Products/schema.graphqls \ - --source-schema-file Reviews/schema.graphqls \ - --source-schema-file Shipping/schema.graphqls \ - --archive gateway.far + -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: @@ -331,42 +271,74 @@ If composition succeeds, copy the updated `gateway.far` to your gateway project cp gateway.far Gateway/gateway.far ``` -### Troubleshooting Composition Errors - -If composition fails after adding your new subgraph, the error messages point to specific issues. Common problems: - -**"Field X is defined in multiple subgraphs without `[Shareable]`"** -- 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 [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) for details. - -**"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. +If you already have a composed `gateway.far` with the Products and Reviews subgraphs, you can add the new subgraph to the existing archive: -**"Incompatible field types"** -- Two subgraphs define the same field with different types. The types must be compatible according to the composition merging rules. +```bash +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 delivery estimates that cross subgraph boundaries: +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 - deliveryEstimate(zip: "10001") + shipments { + trackingNumber + status + } } } ``` -This query touches three subgraphs: +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 and Sharing](/docs/fusion/v16/field-ownership-and-sharing) 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](/docs/fusion/v16/entities-and-lookups). -1. The gateway calls the Products subgraph to fetch `name`, `price`, `weight`, and dimension data -2. The gateway passes the weight and dimensions to the Shipping subgraph as required arguments -3. The Shipping subgraph calculates the delivery estimate and returns it -4. The gateway merges everything into a single response +### Incompatible field types -The client sees one unified response. The `deliveryEstimate` field's `zip` argument is visible to clients, but the `dimension` argument (marked with `[Require]`) is hidden -- the gateway fills it in automatically. +**"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?** If your subgraph's resolvers need data from other subgraphs (like the Shipping subgraph needing product weight), the `[Require]` attribute enables this. Cross-subgraph data dependencies will be covered in detail in future documentation. -- **Composition failed and you don't know why?** See [Composition](/docs/fusion/v16/composition) for the full merging rules, common errors, and fixes. -- **Want to understand entities more deeply?** See [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) for the complete guide to entity stubs, public vs. internal lookups, field ownership, and `[Shareable]`. +- **Need cross-subgraph field dependencies?** See [Data Requirements](/docs/fusion/v16/data-requirements-and-mapping) for the full range of `@require` patterns and FieldSelectionMap syntax. +- **Composition failed?** See [Composition](/docs/fusion/v16/composition) for the full merging rules, common errors, and fixes. +- **Want to understand entities more deeply?** See [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) for the complete guide to entity stubs, public vs. internal lookups, and composite keys. - **Ready to deploy?** See [Deployment and CI/CD](/docs/fusion/v16/deployment-and-ci-cd) for setting up independent subgraph deployments with the Nitro CLI. diff --git a/website/src/docs/fusion/v16/authentication-and-authorization.md b/website/src/docs/fusion/v16/authentication-and-authorization.md index 2d2ca6a0937..aa08e0adf7a 100644 --- a/website/src/docs/fusion/v16/authentication-and-authorization.md +++ b/website/src/docs/fusion/v16/authentication-and-authorization.md @@ -287,7 +287,7 @@ Key details: - The JWT configuration can mirror the gateway's configuration, or the subgraph can validate the forwarded `Authorization` header against the same identity provider. - Subgraphs receive the raw `Authorization` header from the gateway via header propagation, so the JWT middleware validates the same token the gateway already validated. -### Using `[Authorize]` on Fields +### Field-Level Authorization Apply `[Authorize]` to restrict access to specific fields or types: diff --git a/website/src/docs/fusion/v16/composition.md b/website/src/docs/fusion/v16/composition.md index e5abcaecf97..3999624791c 100644 --- a/website/src/docs/fusion/v16/composition.md +++ b/website/src/docs/fusion/v16/composition.md @@ -173,7 +173,7 @@ Why? If one subgraph requires a non-nullable argument, the gateway must always p Not everything in your subgraph schemas should appear in the composite schema. Fusion provides three mechanisms for controlling what clients see. -### `@inaccessible` / `[Inaccessible]` +### Hiding Fields from the Composite Schema Hides a type or field from the client-facing composite schema. The element still exists in the execution schema and can be used internally -- for example, as a source for `[Require]` field dependencies. @@ -207,7 +207,7 @@ type Product @key(fields: "id") { **Constraints:** 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` / `[Internal]` +### Subgraph-Local Elements Declares that a type or field is local to a subgraph and does not participate in standard schema merging. Internal elements do not appear in the composite schema and do not collide with identically-named elements in other subgraphs. @@ -228,7 +228,7 @@ This lookup is available to the gateway for resolving `Product` entity reference The difference from `@inaccessible`: internal elements are completely invisible to the merging process. Two subgraphs can define `[Internal]` fields with the same name and different types without causing a conflict. `@inaccessible` elements still participate in merging -- they just get removed from the final client-facing schema. -### `[Tag]` for Composition Filtering +### Composition Filtering with Tags Tags allow you to label fields and types for organizational purposes and selectively exclude them during composition using the `--exclude-tag` flag. @@ -267,7 +267,7 @@ The `GetRecommendations` field is excluded from the composed schema. This is use - Creating different compositions for different environments (dev includes experimental features, production does not). - Organizing fields by team ownership for filtering. -## The `.far` Archive Format +## The Fusion Archive Format The Fusion archive (`.far` file) is the output of composition and the input to the gateway. It is a binary package containing: @@ -403,7 +403,7 @@ You can also run composition in Nitro cloud as part of your schema delivery pipe These are the errors you will encounter most often, with examples showing what went wrong and how to fix it. -### Field Defined in Multiple Subgraphs Without `[Shareable]` +### Field Defined in Multiple Subgraphs Without Sharing **Error:** `Field "Product.name" is defined in multiple subgraphs without @shareable.` @@ -537,7 +537,7 @@ After merging, the `Purchasable` interface has both `price` and `refundPolicy`. **Fix:** Add the missing field to the implementing type in the appropriate subgraph, or mark the new interface field as `@inaccessible` if it should not be in the composite schema. -### Required Input Field Marked `@inaccessible` +### Required Input Field Hidden from Composite Schema **Error:** `Required input field "CreateProductInput.name" is marked @inaccessible. Required input fields cannot be hidden from the composite schema.` @@ -561,7 +561,7 @@ Products/ If you renamed files, make sure the names match. Run `dotnet run -- schema export` to regenerate both files. -## `schema-settings.json` Reference +## Schema Settings Reference Each subgraph's `schema-settings.json` configures how the subgraph participates in composition. The primary documentation lives on the [Adding a Subgraph](/docs/fusion/v16/adding-a-subgraph) page. Here is a quick reference of the fields relevant to composition: diff --git a/website/src/docs/fusion/v16/data-requirements-and-mapping.md b/website/src/docs/fusion/v16/data-requirements-and-mapping.md index e5ea44dbaa1..8c80bb55ca7 100644 --- a/website/src/docs/fusion/v16/data-requirements-and-mapping.md +++ b/website/src/docs/fusion/v16/data-requirements-and-mapping.md @@ -239,7 +239,7 @@ The path `seller.addresses[countryCode]` means: navigate to `seller.addresses` ( 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 `@provides` Helps +### When Contextual Availability Helps Consider a Reviews subgraph where the `author` field returns a `User` entity. The `User` type and its `username` field are owned by the Accounts subgraph. Normally the gateway would need to call the Accounts subgraph to fetch `username`. But the Reviews subgraph already has the author's username available when resolving `Review.author`. By annotating the `author` field with `@provides(fields: "username")`, the subgraph tells the gateway: "When you resolve `author` through the `Review` entity on my subgraph, I can also give you `username`." @@ -298,7 +298,7 @@ type Review { } ``` -### When to Use `@provides` +### When to Provide Fields Contextually Use `@provides` when: diff --git a/website/src/docs/fusion/v16/deployment-and-ci-cd.md b/website/src/docs/fusion/v16/deployment-and-ci-cd.md index 4cdfb26c064..281fe3141db 100644 --- a/website/src/docs/fusion/v16/deployment-and-ci-cd.md +++ b/website/src/docs/fusion/v16/deployment-and-ci-cd.md @@ -494,7 +494,7 @@ builder.Services # Schema Evolution -## Progressive Field Migration with `[Override]` +## Progressive Field Migration Use `[Override]` to migrate a field from one subgraph to another without breaking existing queries. @@ -534,7 +534,7 @@ public static partial class ProductNode The `[Override]` attribute tells the gateway: "This field used to be resolved by the `products-api` subgraph, but now this subgraph resolves it." The gateway routes queries to the new resolver, and the old resolver is no longer called. -## Excluding Experimental Features with `[Tag]` +## Excluding Experimental Features with Tags Mark fields or types with `[Tag]` to exclude them from composition during development: diff --git a/website/src/docs/fusion/v16/field-ownership-and-sharing.md b/website/src/docs/fusion/v16/field-ownership-and-sharing.md index 289aca7c85e..badab5491e4 100644 --- a/website/src/docs/fusion/v16/field-ownership-and-sharing.md +++ b/website/src/docs/fusion/v16/field-ownership-and-sharing.md @@ -15,7 +15,7 @@ For non-key fields, Fusion expects a single owner. Key fields are special. Fields used for entity identity and lookup mapping can appear in multiple subgraphs as part of entity resolution. -## What `@shareable` Means +## 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. diff --git a/website/src/docs/fusion/v16/getting-started.md b/website/src/docs/fusion/v16/getting-started.md index d871e1780c1..c73adc664d6 100644 --- a/website/src/docs/fusion/v16/getting-started.md +++ b/website/src/docs/fusion/v16/getting-started.md @@ -1020,7 +1020,7 @@ You proved that Fusion works: The Products and Reviews subgraphs are completely independent. They do not import each other's code, do not call each other directly, and can be deployed and scaled separately. Yet clients can query across both as if they were one unified schema. -## Sharing Fields with `[Shareable]` +## 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. diff --git a/website/src/docs/fusion/v16/migration/coming-from-apollo-federation.md b/website/src/docs/fusion/v16/migration/coming-from-apollo-federation.md index 1058eef2672..821ad0004c0 100644 --- a/website/src/docs/fusion/v16/migration/coming-from-apollo-federation.md +++ b/website/src/docs/fusion/v16/migration/coming-from-apollo-federation.md @@ -48,7 +48,7 @@ Several things from Apollo's model have no Fusion equivalent because the archite Beyond naming, several concepts work fundamentally differently in Fusion. Understanding these differences is important for a successful migration. -### Entity Resolution: Lookups vs. `_entities` +### Entity Resolution: Lookups vs. the Entities Query This is the most significant architectural difference between Apollo Federation and Fusion. @@ -135,7 +135,7 @@ The `[Internal]` attribute hides this field from the composite schema. Only the For more on lookups and entity resolution patterns, see [Entities and Lookups](/docs/fusion/v16/entities-and-lookups). -### `@require` Operates on Arguments, Not Fields +### Requirements Operate on Arguments, Not Fields In Apollo Federation, `@requires` is a field-level directive. It declares that a field depends on data from another subgraph: @@ -304,7 +304,7 @@ app.RunWithGraphQLCommands(args); The call to `RunWithGraphQLCommands(args)` enables `dotnet run -- schema export`, which is how Fusion extracts the subgraph schema for composition. -#### Step 2: Convert `@key` + `__resolveReference` to `[Lookup]` +#### 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. @@ -363,7 +363,7 @@ public static Product GetProductById(int id) => new(id); **Key point:** You do not need a `@key` directive. The gateway infers the entity key from the lookup's arguments. If your lookup takes `int id`, the gateway knows `id` is the key. -#### Step 3: Convert `@requires` to `[Require]` +#### Step 3: Convert Field Requirements Replace field-level `@requires` with argument-level `[Require]`. @@ -423,7 +423,7 @@ public int GetDeliveryEstimate( } ``` -#### Step 4: Convert `@external` / `@provides` +#### Step 4: Convert External Fields and Provides **`@external`** has a direct equivalent in `[External]`, but it is less frequently needed. In Apollo, you must mark any field referenced by `@requires` as `@external`. In Fusion, the `[Require]` selection syntax references fields from the composed graph directly -- no `@external` annotation is needed on the entity type. @@ -454,7 +454,7 @@ public static partial class ReviewNode } ``` -#### Step 5: Handle `[Shareable]` +#### 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. @@ -481,7 +481,7 @@ public static partial class UserNode One difference: in Fusion, key fields (like `id`) are automatically shareable. You do not need to annotate them. -#### Step 6: Create `schema-settings.json` +#### 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: @@ -624,7 +624,7 @@ Replace Apollo's `rover` commands with Nitro CLI equivalents. | `rover subgraph publish` | `nitro fusion upload` + `nitro fusion publish` | | `rover supergraph compose` | `nitro fusion compose` | -#### Schema Upload (Replaces `rover subgraph publish`) +#### Schema Upload (Replaces Rover Subgraph Publish) In Apollo, publishing a subgraph triggers server-side composition. In Fusion, this is a two-step process: upload the schema, then publish to trigger composition. @@ -656,7 +656,7 @@ nitro fusion publish \ --api-key $NITRO_API_KEY ``` -#### Schema Validation (Replaces `rover subgraph check`) +#### Schema Validation (Replaces Rover Subgraph Check) **Apollo:** @@ -676,7 +676,7 @@ nitro fusion validate \ --api-key $NITRO_API_KEY ``` -#### Local Composition (Replaces `rover supergraph compose`) +#### Local Composition (Replaces Rover Supergraph Compose) **Apollo:** @@ -763,7 +763,7 @@ nitro fusion compose --archive gateway.far You can run this on your machine, see the output, inspect errors, and fix them before pushing. There is no cloud service in the loop unless you choose to use Nitro cloud. -### `@require` Operates on Arguments, Not Fields +### Requirements Operate on Arguments, Not Fields This changes resolver design. In Apollo, required data appears on the entity object. In Fusion, it arrives as a method parameter: @@ -807,7 +807,7 @@ No. Apollo Federation and Fusion use different gateway protocols and composition 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 directives like `@authenticated` and `@policy`? +### 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. diff --git a/website/src/docs/fusion/v16/migration/migrating-from-schema-stitching.md b/website/src/docs/fusion/v16/migration/migrating-from-schema-stitching.md index 0b397046732..3fdc8338da1 100644 --- a/website/src/docs/fusion/v16/migration/migrating-from-schema-stitching.md +++ b/website/src/docs/fusion/v16/migration/migrating-from-schema-stitching.md @@ -493,7 +493,7 @@ Every subgraph needs a `schema-settings.json` file with the correct: 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 `[Shareable]` +### 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.