docs2/site/docs/getting-started/field-middleware.md
Field Middleware provides additional behaviors during field resolution. GraphQL.NET supports two types:
Both types work similarly to ASP.NET Core HTTP middleware, executing in a chain where each middleware can perform actions before and after the next middleware or resolver.
Middleware is created by implementing the IFieldMiddleware interface:
public class LoggingMiddleware : IFieldMiddleware
{
private readonly ILogger<LoggingMiddleware> _logger;
public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
{
_logger = logger;
}
public async ValueTask<object?> ResolveAsync(IResolveFieldContext context, FieldMiddlewareDelegate next)
{
_logger.LogInformation("Resolving {Field}", context.FieldName);
var result = await next(context);
_logger.LogInformation("Resolved {Field}", context.FieldName);
return result;
}
}
The same middleware class can be used as either global or field-specific middleware depending on how you register it.
Global middleware applies to all fields in your schema. This is useful for cross-cutting concerns like logging, metrics, or authorization.
The recommended approach is using UseMiddleware<T>() on the GraphQL builder:
services.AddGraphQL(b => b
.AddSchema<MySchema>()
.UseMiddleware<InstrumentFieldsMiddleware>()
.UseMiddleware<LoggingMiddleware>());
This automatically registers the middleware in DI as a singleton and applies it to the schema.
You can also register middleware directly on the schema:
public class MySchema : Schema
{
public MySchema(IServiceProvider services, MyQuery query, LoggingMiddleware middleware)
: base(services)
{
Query = query;
FieldMiddleware.Use(middleware);
}
}
Or use a lambda:
schema.FieldMiddleware.Use(next => async context =>
{
// Code before resolver
var result = await next(context);
// Code after resolver
return result;
});
Field-specific middleware applies only to designated fields, offering better performance and clearer intent than global middleware for field-level concerns. There are three ways to apply middleware to a field:
// Register middleware in DI first if applicable
services.AddSingleton<LoggingMiddleware>();
public class MyGraphType : ObjectGraphType
{
public MyGraphType()
{
Field<StringGraphType>("field1")
.Resolve(context => "Data")
// 1. Using a lambda
.ApplyMiddleware(next => async context =>
{
// Custom logic here
var result = await next(context);
return result;
});
Field<StringGraphType>("field2")
.Resolve(context => "Data")
// 2. Using a middleware instance
.ApplyMiddleware(new LoggingMiddleware(logger));
Field<StringGraphType>("field3")
.Resolve(context => "Data")
// 3. Using a type resolved from DI (recommended)
.ApplyMiddleware<LoggingMiddleware>();
}
}
When both global and field-specific middleware are present:
Example:
// Global middleware
services.AddGraphQL(b => b
.AddSchema<MySchema>()
.UseMiddleware<GlobalMiddleware1>()
.UseMiddleware<GlobalMiddleware2>());
// Field-specific middleware
public class MyGraphType : ObjectGraphType
{
public MyGraphType()
{
Field<StringGraphType>("myField")
.Resolve(context => "Result")
.ApplyMiddleware<FieldMiddleware1>()
.ApplyMiddleware<FieldMiddleware2>();
}
}
// Execution order:
// 1. GlobalMiddleware1 (before)
// 2. GlobalMiddleware2 (before)
// 3. FieldMiddleware1 (before)
// 4. FieldMiddleware2 (before)
// 5. Field Resolver executes
// 6. FieldMiddleware2 (after)
// 7. FieldMiddleware1 (after)
// 8. GlobalMiddleware2 (after)
// 9. GlobalMiddleware1 (after)
When using UseMiddleware<T>(), the middleware is automatically registered as a singleton. For manual registration:
services.AddSingleton<LoggingMiddleware>();
public class MySchema : Schema
{
public MySchema(IServiceProvider services, MyQuery query, LoggingMiddleware middleware)
: base(services)
{
Query = query;
FieldMiddleware.Use(middleware);
}
}
When using ApplyMiddleware<T>(), the middleware must be registered in the DI container:
// Register middleware in DI
services.AddSingleton<AuthorizationMiddleware>();
// Apply to field - middleware is resolved from DI during schema initialization
Field<StringGraphType>("protectedField")
.Resolve(context => "Protected data")
.ApplyMiddleware<AuthorizationMiddleware>();
Note: The middleware is resolved from DI during schema initialization, not during each field resolution.
For scoped dependencies, use a singleton middleware and resolve dependencies in ResolveAsync:
public class MyMiddleware : IFieldMiddleware
{
public async ValueTask<object?> ResolveAsync(IResolveFieldContext context, FieldMiddlewareDelegate next)
{
var scopedService = context.RequestServices!
.GetRequiredService<IMyScopedService>();
// Use scoped service
return await next(context);
}
}
Recommended lifetimes for optimal performance:
| Schema | Graph Type | Middleware | Recommendation |
|---|---|---|---|
| singleton | singleton | singleton | ✅ Recommended |
| scoped | scoped | singleton | ⚠️ Less performant |
| scoped | scoped | scoped | ⚠️ Least performant |
| scoped | singleton | scoped | ❌ Avoid - causes duplicate middleware application |
| singleton | singleton | scoped | ❌ Avoid - throws InvalidOperationException |
Important: Middleware is applied during schema initialization. Using incompatible lifetimes can cause middleware to be applied multiple times or fail to resolve.
Use Field Middleware when:
Use Directives when:
For more information, see Directives.
public interface IFieldMiddleware
{
ValueTask<object?> ResolveAsync(IResolveFieldContext context, FieldMiddlewareDelegate next);
}
public delegate ValueTask<object?> FieldMiddlewareDelegate(IResolveFieldContext context);