Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document field middleware #4497

Merged
merged 6 commits into from
Dec 8, 2021
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
WIP: Field middleware
  • Loading branch information
tobias-tengler committed Dec 3, 2021
commit a698b4445760fce1a24391af7fe8f5b5eb9de73b
145 changes: 135 additions & 10 deletions website/src/docs/hotchocolate/execution-engine/field-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
title: Field middleware
---

TODO
TODO introduce field middleware

# Execution order

TODO diagram, data middleware, why order matters

# Definition

Field middleware can be defined either as a delegate or as a separate type.
Field middleware can be defined either as a delegate or as a separate type. In both cases we gain access to a `FieldDelegate` (`next`) and the `IMiddlewareContext` (`context`).

The `IMiddleware` context 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 code.

By awaiting the `FieldDelegate` we are waiting on all other field middleware that might come after the current middleware as well as the actual field resolver, computing the result of the field.

## Field middleware delegate

Expand Down Expand Up @@ -64,8 +72,6 @@ public static class MyMiddlewareObjectFieldDescriptorExtension
```

> Note: We recommend sticking to the convention of prepending `Use` to your extension method to indicate that it is applying a middleware.
>
> For the namespace of this extension class we recommend `HotChocolate.Types`.

You can now use this middleware in different places throughout your schema definition.

Expand All @@ -85,12 +91,89 @@ public class QueryType : ObjectType
}
```

## 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 for example have a scoped 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` is the reason why there isn't a contract like an interface or a base class for field middleware.

### Usage

## Field middleware type
Now that you've defined the middleware as a class we need to still apply it to a field.

TODO
```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` doesn't make as much sense for the `Use<MyMiddleware>` in contrast to the middleware delegate, we still recommend creating on as shown [here](#reusing-the-middleware-delegate). The reason being that you can make easier changes to this middleware in the future without potentially affecting all places you've used this middleware in.
If you need to pass an additional custom argument to the middleware you can do so using the factory overload of the `Use<T>`.

```csharp
descriptor
.Field("example")
.Use((services, next) => new MyMiddleware(next, "custom",
services.GetRequiredService<FooBar>()));
```

While an extension method like `UseMyMiddleware` 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.

# Usage as an attribute

Expand All @@ -109,8 +192,6 @@ public class UseMyMiddlewareAttribute : ObjectFieldDescriptorAttribute
}
```

> Note: You do not have to create an extension method for your middleware first. Of course you can also just create your middleware directly in the `OnConfigure` method using `descriptor.Use`.

The attribute can then be used like the following.

```csharp
Expand All @@ -124,4 +205,48 @@ public class Query
}
```

# Execution order
# 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
}
});
```

# 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 await the `FieldDelegate`.

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