Skip to content

Commit

Permalink
Document field middleware (ChilliCream#4497)
Browse files Browse the repository at this point in the history
  • Loading branch information
tobias-tengler authored Dec 8, 2021
1 parent fe50037 commit 48d071e
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 0 deletions.
1 change: 1 addition & 0 deletions website/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ module.exports = {
options: {
mermaidOptions: {
fontFamily: "sans-serif",
sequence: { showSequenceNumbers: true }
},
},
},
Expand Down
14 changes: 14 additions & 0 deletions website/src/docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@
}
]
},
{
"path": "execution-engine",
"title": "Execution Engine",
"items": [
{
"path": "index",
"title": "Overview"
},
{
"path": "field-middleware",
"title": "Field middleware"
}
]
},
{
"path": "integrations",
"title": "Integrations",
Expand Down
299 changes: 299 additions & 0 deletions website/src/docs/hotchocolate/execution-engine/field-middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
---
title: 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 an `IMiddlewareContext`. It implements the `IResolverContext` interface so you can use all of the `IResolverContext` APIs in your middleware, similarly to how you would use them in your resolver. There are also some special properties like the `Result`, 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 the `UsePagination` and `UseFiltering` 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 is `UsePagination` > `UseFiltering`.

```csharp
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.

```mermaid
sequenceDiagram
UsePagination->>UseFiltering: next(context)
UseFiltering->>Resolver: next(context)
Resolver->>UseFiltering: Result of the Resolver
UseFiltering->>UsePagination: Result of UseFiltering
```

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 a `FieldDelegate`, which allows us to invoke the next middleware, and the `IMiddlewareContext`.

By awaiting the `FieldDelegate` 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.

```csharp
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 resolver
await 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 the `example` field on the `Query` type, but what if you want to use this middleware in multiple places?

You can simply create an extension method for the `IObjectFieldDescriptor`.

```csharp
public static class MyMiddlewareObjectFieldDescriptorExtension
{
public static IObjectFieldDescriptor UseMyMiddleware(
this IObjectFieldDescriptor descriptor)
{
descriptor
.Use(next => async context =>
{
// Omitted code for brevity
await 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.

```csharp
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.

```csharp
public class MyMiddleware
{
private readonly FieldDelegate _next;

public MyMiddleware(FieldDelegate next)
{
_next = next;
}

// this method must be called InvokeAsync or Invoke
public 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 resolver
await _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 the `InvokeAsync` method, if they have a scoped or transient lifetime.

```csharp
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 the `InvokeAsync` 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.

```csharp
public class QueryType : ObjectType
{
protected override void Configure(IObjectTypeDescriptor descriptor)
{
descriptor
.Field("example")
.Use<MyMiddleware>()
.Resolve(context =>
{
// Omitted for brevity
});
}
}
```

While an extension method like `UseMyMiddleware` on the `IObjectFieldDescriptor` doesn't make as much sense for `Use<MyMiddleware>` in contrast to the middleware delegate, we still recommend creating one as shown [here](#reusing-the-middleware-delegate). 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 the `Use` method.

```csharp
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 Annotation-based approach?

You can create a new attribute inheriting from `ObjectFieldDescriptorAttribute` and call or create your middleware inside of the `OnConfigure` 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 proerty 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 the `Use` verb that your attribute is a middleware attribute.
```csharp
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.

```csharp
public class Query
{
[UseMyMiddleware]
public string MyResolver()
{
// Omitted code for brevity
}
}
```

# Accessing the resolver result

The `IMiddlewareContext` conveniently contains a `Result` property that can be used to access the field resolver result.

```csharp
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 resovler 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 result
if (result is string stringResult)
{
// Work with the stringResult
}
});
```

A middleware can also set or override the result by assigning the `context.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 the `IMiddlewareContext`, 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 the `FieldDelegate` (`next`).

```csharp
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 resolver
return Task.CompletedTask;
}
else
{
return next(context);
}
})
```
11 changes: 11 additions & 0 deletions website/src/docs/hotchocolate/execution-engine/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Overview
---

In this section we will learn about the features of the Hot Chocolate execution engine.

# Field middleware

Field middleware allows us to create reusable logic that is run before or after a resolver. It also allows us to access or even modify the result produced by a resolver.

[Learn more about field middleware](/docs/hotchocolate/execution-engine/field-middleware)

0 comments on commit 48d071e

Please sign in to comment.