@link Directive
The @link directive enables schema composition by importing types and directives from external schemas. This powerful feature allows you to build modular GraphQL schemas and is the foundation for specifications like Apollo Federation.
Overview
The @link directive is applied to the schema definition and allows you to:
- Import types and directives from external schema specifications
- Use type aliasing to avoid naming conflicts
- Compose schemas from multiple sources
- Enable federation and other advanced GraphQL patterns
Tanka GraphQL provides complete support for the @link directive including:
- ✅ Basic imports by name
- ✅ Type and directive aliasing
- ✅ Namespace prefixing with
asparameter - ✅ Mixed aliased and non-aliased imports
- ✅ Custom schema loaders
- ✅ Circular reference protection
- ✅ Integration with Apollo Federation
Basic Usage
[Fact]
public async Task BasicLinkUsage()
{
var schema = """
extend schema @link(
url: "https://mycompany.com/schemas/auth/v1.0",
import: ["@authenticated", "@authorized", "Role"]
)
type Query {
profile: User @authenticated
admin: AdminPanel @authorized(role: ADMIN)
}
type User {
id: ID!
name: String
}
type AdminPanel {
settings: String
}
""";
var builtSchema = await new SchemaBuilder()
.Add(schema)
.Build(options =>
{
options.SchemaLoader = new CustomSchemaLoader();
});
Assert.NotNull(builtSchema);
Assert.NotNull(builtSchema.GetNamedType("User"));
Assert.NotNull(builtSchema.GetDirectiveType("authenticated"));
Assert.NotNull(builtSchema.GetNamedType("Role")); // Imported from external schema
}
Syntax
The @link directive follows the GraphQL specification for schema composition:
- url (required): The URL of the specification to import from
- as: Namespace prefix for imported types (e.g.,
federationwould prefix types asfederation__Type) - import: List of specific types and directives to import
- for: Purpose of the link (SECURITY or EXECUTION)
Import Syntax
The import parameter accepts several formats:
Simple Import
Import types and directives by name using string literals.
Import with Aliasing
Rename imported types to avoid conflicts.
[Fact]
public async Task ImportWithAliasing()
{
var schema = """
extend schema @link(
url: "https://mycompany.com/schemas/auth/v1.0",
import: [
{ name: "@authenticated", as: "@requiresAuth" },
{ name: "Role", as: "UserRole" }
]
)
type Query {
profile: User
admin: AdminPanel
}
type User {
id: ID!
name: String
}
type AdminPanel {
settings: String
}
""";
var builtSchema = await new SchemaBuilder()
.Add(schema)
.Build(options =>
{
options.SchemaLoader = new CustomSchemaLoader();
});
Assert.NotNull(builtSchema);
Assert.NotNull(builtSchema.GetNamedType("User"));
// Verify that aliasing worked correctly
Assert.NotNull(builtSchema.GetDirectiveType("requiresAuth")); // Aliased directive
Assert.NotNull(builtSchema.GetNamedType("UserRole")); // Aliased type
Assert.Null(builtSchema.GetDirectiveType("authenticated")); // Original should not exist
Assert.Null(builtSchema.GetNamedType("Role")); // Original should not exist
}
How It Works
The @link directive is processed by the LinkProcessingMiddleware during schema building:
- Directive Discovery: @link directives are extracted from schema definitions and extensions
- URL Resolution: The URL is resolved to determine the appropriate schema loader
- Schema Loading: The schema loader fetches the external specification
- Import Parsing: Import specifications are parsed, including aliases
- Import Filtering: Only the specified imports (or all if none specified) are included
- Type Aliasing: Imported types and directives are renamed according to aliases
- Schema Merging: Imported types are merged into your schema
- Conflict Detection: Ensures no naming conflicts occur
This process supports recursive linking with circular reference protection and maintains type safety throughout the composition process.
Schema Loaders
Tanka GraphQL includes built-in schema loaders:
FederationSchemaLoader
Handles Apollo Federation specifications:
// Automatically configured when using Federation
options.SchemaLoader = new FederationSchemaLoader();
HttpSchemaLoader
Loads schemas from HTTP endpoints:
options.SchemaLoader = new HttpSchemaLoader();
CompositeSchemaLoader
Chains multiple loaders:
options.SchemaLoader = new CompositeSchemaLoader(
new FederationSchemaLoader(),
new HttpSchemaLoader()
);
Examples
Importing Federation Types
[Fact]
public async Task ImportingCustomTypes()
{
var schema = """
extend schema @link(
url: "https://mycompany.com/schemas/validation/v1.0",
import: ["@length", "@range", "@email", "@unique"]
)
type User {
id: ID!
email: String @email @unique
name: String @length(min: 2, max: 50)
age: Int @range(min: 13, max: 120)
}
type Query {
user(id: ID!): User
}
""";
var builtSchema = await new SchemaBuilder()
.Add(schema)
.Build(options =>
{
options.SchemaLoader = new ValidationSchemaLoader();
});
Assert.NotNull(builtSchema);
Assert.NotNull(builtSchema.GetDirectiveType("length"));
Assert.NotNull(builtSchema.GetDirectiveType("range"));
Assert.NotNull(builtSchema.GetDirectiveType("email"));
Assert.NotNull(builtSchema.GetDirectiveType("unique"));
}
Using Namespaces
[Fact]
public async Task UsingNamespaces()
{
var schema = """
extend schema @link(
url: "https://mycompany.com/schemas/auth/v1.0",
as: "auth"
)
type Query {
profile: User @auth__authenticated
admin: AdminPanel @auth__authorized(role: auth__ADMIN)
}
type User {
id: ID!
name: String
}
type AdminPanel {
settings: String
}
""";
var builtSchema = await new SchemaBuilder()
.Add(schema)
.Build(options =>
{
options.SchemaLoader = new CustomSchemaLoader();
});
Assert.NotNull(builtSchema);
Assert.NotNull(builtSchema.GetNamedType("User"));
// Note: Namespace prefixing implementation may need verification
}
Custom Specifications
You can import from your own specifications:
[Fact]
public async Task CustomSpecifications()
{
var schema = """
extend schema @link(
url: "https://mycompany.com/schemas/auth/v1.0",
import: ["@authenticated", "@authorized", "Role"]
)
type Query {
profile: User @authenticated
admin: AdminPanel @authorized(role: ADMIN)
}
type User {
id: ID!
name: String
}
type AdminPanel {
settings: String
}
enum Role {
USER
ADMIN
}
""";
// Note: This would require a custom schema loader for the auth specification
var builtSchema = await new SchemaBuilder()
.Add(schema)
.Build();
Assert.NotNull(builtSchema);
Assert.NotNull(builtSchema.GetNamedType("User"));
Assert.NotNull(builtSchema.GetNamedType("AdminPanel"));
}
Creating Custom Schema Loaders
Implement ISchemaLoader to load schemas from custom sources:
[Fact]
public void CustomSchemaLoaderExample()
{
// Example of implementing a custom schema loader
var customLoader = new CustomSchemaLoader();
var options = new SchemaBuildOptions();
options.SchemaLoader = new CompositeSchemaLoader(
customLoader,
new HttpSchemaLoader()
);
// Verify the loader chain is set up correctly
Assert.NotNull(options.SchemaLoader);
Assert.IsType<CompositeSchemaLoader>(options.SchemaLoader);
}
Processing Pipeline
The @link directive is processed by the LinkProcessingMiddleware during schema building:
- Initialization Stage: Basic schema setup
- Type Collection: Gather type definitions
- Link Processing Stage:
- Process all
@linkdirectives - Load external schemas
- Apply import filtering
- Handle type aliasing
- Merge imported types
- Process all
- Type Resolution: Configure resolvers
- Validation: Validate complete schema
- Finalization: Create executable schema
Best Practices
Be Specific with Imports: Only import what you need to avoid namespace pollution
[Fact] public async Task SpecificImportsBestPractice() { // Best practice: Be specific with imports to avoid namespace pollution var schema = """ extend schema @link( url: "https://mycompany.com/schemas/auth/v1.0", import: ["@authenticated", "@authorized"] # Only import what you need )
type Query { profile: User @authenticated admin: AdminPanel @authorized(role: ADMIN) } type User { id: ID! name: String } type AdminPanel { settings: String } enum Role { USER ADMIN } """; var builtSchema = await new SchemaBuilder() .Add(schema) .Build(options => { options.SchemaLoader = new CustomSchemaLoader(); }); Assert.NotNull(builtSchema); Assert.NotNull(builtSchema.GetDirectiveType("authenticated")); Assert.NotNull(builtSchema.GetDirectiveType("authorized")); // @notused should NOT be available since it wasn't imported (even though it exists in the loaded schema) Assert.Null(builtSchema.GetDirectiveType("notused"));}
Use Aliasing for Conflicts: When importing from multiple sources, use aliases to avoid naming conflicts
Version Your Specifications: Include version numbers in URLs for stability
[Fact] public async Task VersionedSpecificationsBestPractice() { // Best practice: Include version numbers in URLs for stability var schema = """ extend schema @link( url: "https://mycompany.com/schemas/auth/v1.0", # Specific version import: ["@authenticated"] )
type Query { profile: User @authenticated } type User { id: ID! name: String } """; var builtSchema = await new SchemaBuilder() .Add(schema) .Build(options => { options.SchemaLoader = new CustomSchemaLoader(); }); Assert.NotNull(builtSchema); Assert.NotNull(builtSchema.GetDirectiveType("authenticated"));}
Cache External Schemas: Implement caching in custom loaders for performance
Validate Imports: Ensure imported types are used correctly in your schema
Troubleshooting
Common Issues
- Schema not loading: Verify the URL is accessible and the schema loader is configured
- Type conflicts: Use aliasing or namespaces to resolve naming conflicts
- Missing imports: Check that all required types are included in the import list
- Circular dependencies: Avoid schemas that link to each other recursively
Debugging
Enable debug logging to trace @link processing by configuring logging at the Debug level in your application.
Limitations
- Recursive linking depth is limited to prevent infinite loops
- Schema loaders must return valid GraphQL SDL
- Imported types must not conflict with existing types unless aliasing is used
See Also
- xref:types:03-middleware.md[Middleware Pipeline] - How @link processing fits in the build pipeline
- xref:extensions:apollo-federation.md[Apollo Federation] - Using @link for federation
- xref:types:01_1-builder.md[Schema Builder] - Configuring schema loaders