Apollo Federation

"Implement a single data graph across multiple services"

Apollo Federation v2.3 Specification

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:

  1. Define your schema with federation directives using @link
  2. Configure reference resolvers for entity resolution
  3. 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

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 @key directive 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

  1. 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()));
         });
    

    }

  2. 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));
         });
    

    }

  3. Optimize key selection - Choose efficient key fields for entity resolution

  4. Test compatibility - Use the Apollo Federation compatibility suite to validate your subgraph

  5. 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 _Any and FieldSet scalars
  • Configures the Federation schema loader
  • Adds initialization middleware to process @link directives
  • Adds configuration middleware to set up entity resolution