Apollo Federation
"Implement a single data graph across multiple services"
Overview
Tanka GraphQL provides comprehensive support for Apollo Federation v2.3, implementing the subgraph specification that allows GraphQL services to be composed into a unified supergraph managed by an Apollo Gateway or Router.
Key features include:
- Full Apollo Federation v2.3 compliance with @link directive support
- Middleware pipeline architecture for seamless integration
- Automatic schema generation with proper SDL filtering
- Type aliasing support for @link imports to avoid naming conflicts
- Reference resolvers for entity federation
- Built-in compatibility testing with Apollo Federation subgraph compatibility suite
Installation
Support is provided as a NuGet package:
dotnet add package Tanka.GraphQL.Extensions.ApolloFederation
Quick Start
Creating a federated subgraph involves three main steps:
- Define your schema with federation directives using
@link - Configure reference resolvers for entity resolution
- Add federation support to your schema builder
Basic Example
public static async Task BasicFederationExample()
{
// 1. Define schema with @link directive for Federation v2.3
var schema = """
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key", "@external", "@requires", "@provides"])
type Product @key(fields: "id") {
id: ID!
name: String
price: Float
}
type Query {
product(id: ID!): Product
}
""";
// 2. Configure reference resolvers for entity resolution
var referenceResolvers = new DictionaryReferenceResolversMap
{
["Product"] = (context, type, representation) =>
{
var id = representation.GetValueOrDefault("id")?.ToString();
var product = GetProductById(id); // Your data access logic
return ValueTask.FromResult(new ResolveReferenceResult(type, product));
}
};
// 3. Build executable schema with federation support
var executableSchema = await new ExecutableSchemaBuilder()
.Add(schema)
.Add("Query", new FieldsWithResolvers
{
["product(id: ID!): Product"] = b => b.Run(context =>
{
var id = context.GetArgument<string>("id");
context.ResolvedValue = GetProductById(id);
return ValueTask.CompletedTask;
})
})
.Build(options =>
{
options.UseFederation(new SubgraphOptions(referenceResolvers));
});
}
Advanced Features
Schema Composition with @link
Apollo Federation v2.3 uses the @link directive for schema composition. Tanka GraphQL automatically processes these directives and imports the required types and directives.
Type Aliasing
You can use aliases to avoid naming conflicts when importing federation types. Aliasing allows you to rename imported directives and types:
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3",
import: [{name: "@key", as: "@primaryKey"}, "@external"])
type Product @primaryKey(fields: "id") {
id: ID!
name: String
}
The @link directive supports importing with aliases using the object syntax:
{name: "@key", as: "@primaryKey"}imports the@keydirective as@primaryKey- Simple string imports like
"@external"use the original name
See the xref:types:14-link-directive.md[@link directive documentation] for complete details on schema composition and aliasing.
Middleware Pipeline Integration
Federation support is seamlessly integrated into the schema builder's middleware pipeline:
public static async Task MiddlewarePipelineIntegration()
{
var schemaSDL = """
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key", "@external"])
type Product @key(fields: "id") {
id: ID!
name: String
}
type Query {
product(id: ID!): Product
}
""";
var resolversBuilder = new ResolversBuilder();
resolversBuilder.Resolver("Query", "product").Run(context =>
{
context.ResolvedValue = new { id = "1", name = "Test Product" };
return ValueTask.CompletedTask;
});
var subgraphOptions = new SubgraphOptions(new DictionaryReferenceResolversMap());
var schema = await new SchemaBuilder()
.Add(schemaSDL)
.Build(options =>
{
options.Resolvers = resolversBuilder.BuildResolvers();
options.UseFederation(subgraphOptions);
});
// The federation middleware automatically:
// - Processes @link directives and imports required types
// - Adds _service and _entities fields to the Query type
// - Configures entity resolution based on your reference resolvers
// - Generates proper subgraph SDL for federation
}
Reference Resolvers
Reference resolvers handle entity resolution when the gateway requests entities by their key fields:
public static async Task ReferenceResolverExample()
{
var schema = """
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key"])
type Product @key(fields: "id") @key(fields: "sku") {
id: ID!
sku: String!
name: String
}
type Query {
product(id: ID!): Product
}
""";
// Reference resolvers handle entity resolution when the gateway requests entities by their key fields
var referenceResolvers = new DictionaryReferenceResolversMap
{
["Product"] = async (context, type, representation) =>
{
// Extract key fields from representation
var id = representation.GetValueOrDefault("id")?.ToString();
var sku = representation.GetValueOrDefault("sku")?.ToString();
// Resolve entity based on key fields
Product? product = null;
if (id != null)
{
product = await GetProductByIdAsync(id);
}
else if (sku != null)
{
product = await GetProductBySkuAsync(sku);
}
return new ResolveReferenceResult(type, product);
}
};
var executableSchema = await new ExecutableSchemaBuilder()
.Add(schema)
.Build(options =>
{
options.UseFederation(new SubgraphOptions(referenceResolvers));
});
}
Federation Directives
Tanka GraphQL supports all Apollo Federation v2.3 directives:
public static async Task FederationDirectivesExample()
{
// Tanka GraphQL supports all Apollo Federation v2.3 directives
var schema = """
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key", "@external", "@requires", "@provides",
"@shareable", "@inaccessible", "@override",
"@tag", "@extends", "@composeDirective", "@interfaceObject"])
type Product @key(fields: "id") @shareable {
id: ID!
name: String @inaccessible
price: Float @tag(name: "public")
weight: Float @external
shippingCost: Float @requires(fields: "weight")
}
type Review @key(fields: "id") {
id: ID!
product: Product @provides(fields: "name")
rating: Int
}
type Query {
product(id: ID!): Product
}
""";
var executableSchema = await new ExecutableSchemaBuilder()
.Add(schema)
.Build(options =>
{
options.UseFederation(new SubgraphOptions(new DictionaryReferenceResolversMap()));
});
}
The supported directives include:
@key- Define entity key fields@external- Mark fields as owned by other subgraphs@requires- Specify required fields for computed fields@provides- Indicate which fields a resolver provides@shareable- Allow multiple subgraphs to resolve the same field@inaccessible- Hide fields from the public schema@override- Take ownership of a field from another subgraph@tag- Add metadata tags for tooling@extends- Extend types defined in other subgraphs@composeDirective- Include custom directives in composition@interfaceObject- Transform interfaces into object types
Compatibility Testing
Tanka GraphQL includes comprehensive compatibility testing with the official Apollo Federation subgraph compatibility suite. You can run the compatibility tests for your own subgraph:
# Run the Apollo Federation compatibility sample
cd samples/GraphQL.Samples.ApolloFederation.Compatibility
dotnet run
Migration from v1
If you're migrating from Apollo Federation v1, update your schema to use the @link directive:
public static async Task MigrationFromV1Example()
{
// Federation v2.3 - Use @link directive instead of manually defining federation types
var schemaV2 = """
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key", "@external"])
type Product @key(fields: "id") {
id: ID!
name: String
}
type Query {
product(id: ID!): Product
}
""";
var executableSchema = await new ExecutableSchemaBuilder()
.Add(schemaV2)
.Build(options =>
{
options.UseFederation(new SubgraphOptions(new DictionaryReferenceResolversMap()));
});
// The middleware automatically handles the migration and adds the required fields
}
Best Practices
Use specific imports - Only import the federation directives you actually use:
public static async Task SpecificImportsExample() { // Best practice: Only import the federation directives you actually use var schema = """ extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@shareable"])
type Product @key(fields: "id") @shareable { id: ID! name: String } type Query { product(id: ID!): Product } """; var executableSchema = await new ExecutableSchemaBuilder() .Add(schema) .Build(options => { options.UseFederation(new SubgraphOptions(new DictionaryReferenceResolversMap())); });}
Handle null entities - Reference resolvers should handle cases where entities don't exist:
public static async Task ErrorHandlingExample() { var schema = """ extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
type Product @key(fields: "id") { id: ID! name: String } type Query { product(id: ID!): Product } """; // Handle null entities - Reference resolvers should handle cases where entities don't exist var referenceResolvers = new DictionaryReferenceResolversMap { ["Product"] = (context, type, representation) => { var id = representation.GetValueOrDefault("id")?.ToString(); // Handle missing entities gracefully if (string.IsNullOrEmpty(id)) { return ValueTask.FromResult(new ResolveReferenceResult(type, null)); } var product = GetProductById(id); if (product == null) { // Return null for non-existent entities return ValueTask.FromResult(new ResolveReferenceResult(type, null)); } return ValueTask.FromResult(new ResolveReferenceResult(type, product)); } }; var executableSchema = await new ExecutableSchemaBuilder() .Add(schema) .Build(options => { options.UseFederation(new SubgraphOptions(referenceResolvers)); });}
Optimize key selection - Choose efficient key fields for entity resolution
Test compatibility - Use the Apollo Federation compatibility suite to validate your subgraph
Monitor performance - Entity resolution can impact query performance in large federations
Troubleshooting
Common Issues
- Missing @link directive: Ensure your schema includes the @link directive with the correct Federation v2.3 URL
- Entity resolution errors: Check that your reference resolvers handle all key combinations
- Type conflicts: Use aliasing in @link imports to resolve naming conflicts
- Schema validation errors: Verify that all imported directives are properly used in your schema
Federation Schema Loader
Tanka GraphQL includes a FederationSchemaLoader that automatically loads Federation v2.3 types when processing @link directives pointing to Apollo Federation specifications. This loader is automatically configured when you use UseFederation().
API Reference
SubgraphOptions
Configure your subgraph with reference resolvers:
var options = new SubgraphOptions(referenceResolvers)
{
// Optionally specify a different Federation version
FederationSpecUrl = "https://specs.apollo.dev/federation/v2.3",
// Optionally specify which types to import (null imports all)
ImportList = new[] { "@key", "@external", "@requires" }
};
UseFederation Extension
Add Federation support to your schema builder:
options.UseFederation(subgraphOptions);
This extension method:
- Adds Federation value converters for
_AnyandFieldSetscalars - Configures the Federation schema loader
- Adds initialization middleware to process
@linkdirectives - Adds configuration middleware to set up entity resolution