For up-to-date documentation, see thelatest stable version.
Field middleware
The field middleware is one of the fundamental components in Hot Chocolate. It allows you to create reuseable logic that can be run before or after a field resolver. Field middleware is composable, so you can specify multiple middleware and they will be executed in order. The field resolver is always the last element in this middleware chain.
Each field middleware only knows about the next element in the chain and can choose to
- execute logic before it
- execute logic after all later components (including the field resolver) have been run
- not execute the next component
Each field middleware also has access to anIMiddlewareContext
. It implements theIResolverContext
interface so you can use all of theIResolverContext
APIs in your middleware, similarly to how you would use them in your resolver. There are also some special properties like theResult
, which holds the resolver or middleware computed result.
Middleware order
If you have used Hot Chocolate's data middleware before you might have encountered warnings about the order of middleware. The order is important, since it determines in which order the middleware are executed, e.g. in which order the resolver result is being processed.
Take theUsePagination
andUseFiltering
middleware for example: Does it make sense to first paginate and then filter? No. It should first be filtered and then paginated. That's why the correct order isUsePagination
>UseFiltering
.
descriptor.UsePagination().UseFiltering().Resolve(context =>{// Omitted code for brevity});
But hold up, isn't this the opposite order of what we've just described?
Lets visualize the middleware chain to understand why it is indeed the correct order.
As you can see the result of the resolver flows backwards through the middleware. So the middleware is first invoked in the order they were defined, but the result produced by the last middleware, the field resolver, is sent back to first middleware in reverse order.
Definition
Field middleware can be defined either as a delegate or as a separate type. In both cases we gain access to aFieldDelegate
, which allows us to invoke the next middleware, and theIMiddlewareContext
.
By awaiting theFieldDelegate
we are waiting for the completion of all of the middleware that might come after the current middleware, including the actual field resolver.
Field middleware delegate
A field middleware delegate can be defined using code-first APIs.
public class QueryType : ObjectType{protected override void Configure(IObjectTypeDescriptor descriptor){descriptor.Field("example").Use(next => async context =>{// Code up here is executed before the following middleware// and the actual field resolver// This invokes the next middleware// or if we are at the last middleware the field resolverawait next(context);// Code down here is executed after all later middleware// and the actual field resolver has finished executing}).Resolve(context =>{// Omitted for brevity});}}
Reusing the middleware delegate
As it's shown above the middleware is only applied to theexample
field on theQuery
type, but what if you want to use this middleware in multiple places?
You can simply create an extension method for theIObjectFieldDescriptor
.
public static class MyMiddlewareObjectFieldDescriptorExtension{public static IObjectFieldDescriptor UseMyMiddleware(this IObjectFieldDescriptor descriptor){descriptor.Use(next => async context =>{// Omitted code for brevityawait next(context);// Omitted code for brevity});}}
Note: We recommend sticking to the convention of prepending
Use
to your extension method to indicate that it is applying a middleware.
You can now use this middleware in different places throughout your schema definition.
public class QueryType : ObjectType{protected override void Configure(IObjectTypeDescriptor descriptor){descriptor.Field("example").UseMyMiddleware().Resolve(context =>{// Omitted for brevity});}}
Field middleware as a class
If you do not like using a delegate, you can also create a dedicated class for your middleware.
public class MyMiddleware{private readonly FieldDelegate _next;public MyMiddleware(FieldDelegate next){_next = next;}// this method must be called InvokeAsync or Invokepublic async Task InvokeAsync(IMiddlewareContext context){// Code up here is executed before the following middleware// and the actual field resolver// This invokes the next middleware// or if we are at the last middleware the field resolverawait _next(context);// Code down here is executed after all later middleware// and the actual field resolver has finished executing}}
If you need to access services you can either inject them via the constructor, if they are singleton, or as an argument of theInvokeAsync
method, if they have a scoped or transient lifetime.
public class MyMiddleware{private readonly FieldDelegate _next;private readonly IMySingletonService _singletonService;public MyMiddleware(FieldDelegate next, IMySingletonService singletonService){_next = next;_singletonService = singletonService;}public async Task InvokeAsync(IMiddlewareContext context,IMyScopedService scopedService){// Omitted code for brevity}}
The ability to add additional arguments to theInvokeAsync
method is the reason why there isn't a contract like an interface or a base class for field middleware.
Usage
Now that you've defined the middleware as a class we need to still apply it to a field.
public class QueryType : ObjectType{protected override void Configure(IObjectTypeDescriptor descriptor){descriptor.Field("example").Use<MyMiddleware>().Resolve(context =>{// Omitted for brevity});}}
While an extension method likeUseMyMiddleware
on theIObjectFieldDescriptor
doesn't make as much sense forUse<MyMiddleware>
in contrast to the middleware delegate, we still recommend creating one as shownhere. The reason being that you can make changes to this middleware more easily in the future without potentially having to change all places this middleware is being used in.
If you need to pass an additional custom argument to the middleware you can do so using the factory overload of theUse
method.
descriptor.Field("example").Use((provider, next) => new MyMiddleware(next, "custom",provider.GetRequiredService<FooBar>()));
Usage as an attribute
Up until now we have only worked with code-first APIs to create the field middleware. What if you want to apply your middleware to a field resolver defined using the implementation-first approach?
You can create a new attribute inheriting fromObjectFieldDescriptorAttribute
and call or create your middleware inside of theOnConfigure
method.
Note: Attribute order is not guaranteed in C#, so we, in the case of middleware attributes, use the
CallerLineNumberAttribute
to inject the C# line number at compile time. The line number is used as an order. We do not recommend inheriting middleware attributes from a base method or property since this can lead to confusion about ordering. Look at the example below to see how we infer the order. When inheriting from middleware, attributes always pass through the order argument. Further, indicate with theUse
verb that your attribute is a middleware attribute.
public class UseMyMiddlewareAttribute : ObjectFieldDescriptorAttribute{public UseMyMiddlewareAttribute([CallerLineNumber] int order = 0){Order = order;}public override void OnConfigure(IDescriptorContext context,IObjectFieldDescriptor descriptor, MemberInfo member){descriptor.UseMyMiddleware();}}
The attribute can then be used like the following.
public class Query{[UseMyMiddleware]public string MyResolver(){// Omitted code for brevity}}
Accessing the resolver result
TheIMiddlewareContext
conveniently contains aResult
property that can be used to access the field resolver result.
descriptor.Use(next => async context =>{await next(context);// It only makes sense to access the result after calling// next(context), i.e. after the field resolver and any later// middleware has finished executing.object? result = context.Result;// If needed you can now narrow down the type of the result// using pattern matching and continue with the typed resultif (result is string stringResult){// Work with the stringResult}});
A middleware can also set or override the result by assigning thecontext.Result
property.
Note: The field resolver will only execute if no result has been produced by one of the preceding field middleware. If any middleware has set the
Result
property on theIMiddlewareContext
, the field resolver will be skipped.
Short-circuiting
In some cases we might want to short-circuit the execution of field middleware / the field resolver. For this we can simply not call theFieldDelegate
(next
).
descriptor.Use(next => context =>{if(context.Parent<object>() is IDictionary<string, object> dict){context.Result = dict[context.Field.Name];// We are not executing any of the later middleware// or the field resolverreturn Task.CompletedTask;}else{return next(context);}})