Field Middleware

Field Middleware provides additional behaviors during field resolution. GraphQL.NET supports two types:

  1. Global Middleware - Applied to all fields across the entire schema
  2. Field-Specific Middleware - Applied to individual fields (v8.7.0+)

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.

Creating Middleware

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

Global middleware applies to all fields in your schema. This is useful for cross-cutting concerns like logging, metrics, or authorization.

Using UseMiddleware

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.

Manual Registration

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

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

Execution Order

When both global and field-specific middleware are present:

  1. Global middleware (in registration order)
  2. Field-specific middleware (in application order)
  3. Field resolver

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)

Dependency Injection

Using DI with Global Middleware

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

Using DI with Field-Specific 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.

Scoped Dependencies

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

Lifetime Considerations

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.

Field Middleware vs Directives

Use Field Middleware when:

  • You need programmatic control over field behavior
  • The behavior is implementation-specific (not part of the schema contract)
  • You want to apply logic to specific fields without schema changes

Use Directives when:

  • The behavior should be visible in schema introspection
  • You want schema-first configuration
  • The behavior applies to multiple schema elements (types, fields, arguments)

For more information, see Directives.

Interface Reference

public interface IFieldMiddleware { ValueTask<object?> ResolveAsync(IResolveFieldContext context, FieldMiddlewareDelegate next); } public delegate ValueTask<object?> FieldMiddlewareDelegate(IResolveFieldContext context);