@defer and @stream Directives Usage Guide
This guide covers the practical usage of @defer and @stream directives in Tanka GraphQL for incremental delivery.
Server Configuration
Service Registration
Full Incremental Delivery Support
services.AddDefaultTankaGraphQLServices();
services.AddIncrementalDeliveryDirectives(); // Adds both @defer and @stream
Individual Directive Support
services.AddDefaultTankaGraphQLServices();
services.AddDeferDirective(); // Only @defer directive
services.AddStreamDirective(); // Only @stream directive
Schema Configuration
Executable Schema Builder
var schema = await new ExecutableSchemaBuilder()
.AddIncrementalDeliveryDirectives() // Registers both directives
.Add("Query", queryResolvers)
.Build();
Manual Registration
var schema = await new ExecutableSchemaBuilder()
.AddDeferDirective()
.AddStreamDirective()
.Add("Query", queryResolvers)
.Build();
@defer Directive Usage
For a complete, working example of @defer usage, see: Defer Tutorial Basic Usage
Syntax Variations
Basic Defer
{
user {
id
name
... @defer {
profile {
bio
avatar
}
}
}
}
Labeled Defer
{
user {
id
name
... @defer(label: "userProfile") {
profile {
bio
avatar
}
}
}
}
Variable Labels
query GetUser($profileLabel: String!) {
user {
id
name
... @defer(label: $profileLabel) {
profile {
bio
avatar
}
}
}
}
Multiple Deferred Fragments
{
user {
id
name
... @defer(label: "profile") {
profile {
bio
avatar
}
}
... @defer(label: "posts") {
posts {
title
content
}
}
... @defer(label: "friends") {
friends {
id
name
}
}
}
}
Nested Defer
{
user {
id
name
... @defer(label: "level1") {
profile {
bio
... @defer(label: "level2") {
settings {
theme
notifications
}
}
}
}
}
}
@stream Directive Usage
For a complete, working example of @stream usage, see: Stream Tutorial Basic Usage
Syntax Variations
Basic Stream
{
products @stream {
id
name
price
}
}
With Initial Count
{
products @stream(initialCount: 3) {
id
name
price
description
}
}
Stream All Items
{
products @stream(initialCount: 0) {
id
name
price
}
}
Labeled Stream
{
products @stream(initialCount: 2, label: "productList") {
id
name
price
}
}
Stream with Nested Data
{
categories @stream(initialCount: 1) {
id
name
products {
id
name
price
}
}
}
Conditional Streaming
query GetProducts($shouldStream: Boolean = false, $initialCount: Int = 5) {
products @stream(initialCount: $initialCount) @skip(if: $shouldStream) {
id
name
price
}
# Fallback for non-streaming
allProducts: products @include(if: $shouldStream) {
id
name
price
}
}
Combined Usage Patterns
Defer + Stream Together
For a complete example combining both directives, see: Combined Defer and Stream Tutorial
{
user {
id
name
... @defer(label: "posts") {
posts @stream(initialCount: 3) {
id
title
content
... @defer(label: "postStats") {
likes
comments
shares
}
}
}
}
}
Complex Data Hierarchies
{
dashboard {
# Critical data first
user {
id
name
}
# Analytics deferred
... @defer(label: "analytics") {
analytics {
totalViews
# Large datasets streamed
topPages @stream(initialCount: 5) {
url
views
# Expensive metrics deferred per item
... @defer(label: "pageMetrics") {
bounceRate
timeOnPage
conversions
}
}
}
}
# Recent activity streamed
... @defer(label: "activity") {
recentActivity @stream(initialCount: 2) {
id
type
timestamp
description
}
}
}
}
Field Resolution Patterns
Proper Resolver Implementation
Collection Resolvers for @stream
// ✅ Correct: Return actual collection
.Add("products: [Product]", b => b.ResolveAs(productList))
// ❌ Incorrect: Returns function instead of collection
.Add("products: [Product]", b => b.ResolveAs(() => productList))
Async Resolvers for @defer
.Add("User", new()
{
{ "id: ID!", b => b.ResolveAsPropertyOf<User>(u => u.Id) },
{ "name: String!", b => b.ResolveAsPropertyOf<User>(u => u.Name) },
{
"profile: Profile",
async context =>
{
// Expensive operation suitable for deferring
await Task.Delay(100);
var profile = await profileService.GetProfileAsync(context.Parent<User>().Id);
context.ResolvedValue = profile;
}
}
})
Response Structure Examples
@defer Response Flow
Initial Response
{
"data": {
"user": {
"id": "1",
"name": "John Doe"
}
},
"hasNext": true
}
Incremental Response
{
"incremental": [
{
"label": "userProfile",
"path": ["user"],
"data": {
"profile": {
"bio": "Software developer",
"avatar": "https://example.com/avatar.jpg"
}
}
}
],
"hasNext": false
}
@stream Response Flow
Initial Response
{
"data": {
"products": [
{"id": "1", "name": "Product 1", "price": 100},
{"id": "2", "name": "Product 2", "price": 200}
]
},
"hasNext": true
}
Incremental Responses
{
"incremental": [
{
"path": ["products"],
"items": [
{"id": "3", "name": "Product 3", "price": 300}
]
}
],
"hasNext": true
}
{
"incremental": [
{
"path": ["products"],
"items": [
{"id": "4", "name": "Product 4", "price": 400},
{"id": "5", "name": "Product 5", "price": 500}
]
}
],
"hasNext": false
}
Error Handling
Partial Errors in Deferred Data
{
"incremental": [
{
"label": "userProfile",
"path": ["user"],
"data": {
"profile": null
},
"errors": [
{
"message": "Profile service unavailable",
"path": ["user", "profile"]
}
]
}
],
"hasNext": false
}
Stream Item Errors
{
"incremental": [
{
"path": ["products"],
"items": [null, null, {"id": "3", "name": "Product 3", "price": 300}],
"errors": [
{
"message": "Product not found",
"path": ["products", 0]
},
{
"message": "Access denied",
"path": ["products", 1]
}
]
}
],
"hasNext": true
}
Performance Optimization
Batching Strategies
- Group related deferred fields under single labels
- Use appropriate
initialCountvalues for streams - Consider network round-trip costs vs. data size
Memory Management
- Stream large datasets instead of loading everything
- Use defer for expensive computations
- Monitor server memory usage during streaming
Caching Considerations
- Incremental responses complicate caching
- Consider caching strategies for initial vs. deferred data
- Use labels consistently for cache key generation
Testing Strategies
Unit Testing Resolvers
Complete unit tests can be found in:
Integration Testing
Complete integration tests can be found in:
Manual Testing Commands
Test @defer
curl -X POST http://localhost:5000/graphql \
-H "Content-Type: application/json" \
-H "Accept: multipart/mixed" \
-d '{"query": "{ user { id name ... @defer(label: \"profile\") { profile { email } } } }"}'
Test @stream
curl -X POST http://localhost:5000/graphql \
-H "Content-Type: application/json" \
-H "Accept: multipart/mixed" \
-d '{"query": "{ products @stream(initialCount: 2) { id name } }"}'