Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
51 changes: 37 additions & 14 deletions docs/advanced/subscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ sidebar_label: Subscriptions

## Initial Setup

Successfully handling subscriptions in your GraphQL AspNet server can be straight forward for single server environments or very complicated for multi-server and scalable solutions. First we'll look at adding subscriptions for a single server.
Successfully handling subscriptions in your GraphQL AspNet server can be straight forward for single server environments or very complicated for multi-server and scalable solutions. First we'll look at adding subscriptions for a single server.

### Install the Subscriptions Package

The first step to using subscriptions is to install the subscription server package.

```Powershell
Expand All @@ -23,7 +24,6 @@ You must configure web socket support for your Asp.Net server instance separatel

After web sockets are added to your server, add subscription support to the graphql registration.


```C#
// startup.cs

Expand All @@ -47,6 +47,7 @@ After web sockets are added to your server, add subscription support to the grap
> Don't forget to add `UseWebsockets` in the `Configure` method of startup.cs

### Create a Subscription

Declaring a subscription is the same as declaring a query or mutation on a controller but with `[Subscription]` and `[SubscriptionRoot]` attributes.

```C#
Expand All @@ -63,11 +64,13 @@ public class SubscriptionController : GraphController
}
}
```

> Subscriptions can be asyncronous and return a Task<IGraphActionResult> as well.

Here we've declared a new subscription the server will respond to, one that takes in a `filter` parameter to restrict the data that any subscribers receive.

A query to invoke this subscription may look like this:

```javascript
subscription {
onWidgetChanged(filter: "Big"){
Expand All @@ -77,12 +80,13 @@ subscription {
}
}
```

Any updated widgets that start with the phrase "Big" will then be sent to the requestor as they are changed on the server.

### Publish a Subscription Event

In order for the subscription server to send data to any subscribers it has to be notified when something changes. It does this via named Subscription Events. These are internal, unique keys that identify when something happened, usually via a mutation. Once the mutation publishes an event, the subscription server will inspect the published data and, assuming the data type matches the expected data for the subscription, it will execute the subscription method for any connected subscribers and deliver the results as necessary.


```C#
public class MutationController : GraphController
{
Expand All @@ -106,6 +110,7 @@ public class MutationController : GraphController
> Notice that the event name used in `PublishSubscriptionEvent` is the same as the `EventName` property on the `[SubscriptionRoot]` attribute. The subscription server will use the published event name to match which registered subscriptions need to receive the data being published.

### Subscription Event Source Data

In the example above, the data sent with `PublishSubscriptionEvent` is the same as the first input parameter called `eventData` which is the same as the field return type of the controller method. By default, the subscription will look for a parameter with the same data type as its field return type and use that as the event data source.

You can explicitly flag a different parameter, or a parameter of a different data type to be the expected event source with the `[SubscriptionSource]` attribute.
Expand All @@ -127,11 +132,13 @@ public class SubscriptionController : GraphController
}
}
```
Here the subscription expects that an event is published using a `WidgetInternal` data type that it will internally convert to a `Widget` and send to any subscribers. This can be useful if you wish to share internal objects between your mutations and subscriptions that you don't want publicly exposed.

Here the subscription expects that an event is published using a `WidgetInternal` data type that it will internally convert to a `Widget` and send to any subscribers. This can be useful if you wish to share internal objects between your mutations and subscriptions that you don't want publicly exposed.

### Summary

That's all there is for a basic subscription server setup.

1. Add the package reference and update startup.cs
2. Create a new subscription using `[Subscription]` or `[SubscriptionRoot]`
3. Publish an event from a mutation
Expand All @@ -141,16 +148,19 @@ That's all there is for a basic subscription server setup.
A complete example of single instance subscription server including a react app that utilizes the Apollo Client is available in the [demo projects](../reference/demo-projects) section.

## Scaling Subscription Servers

Using web sockets has a natural limitation in that any each server instance has a maximum number of socket connections that it can handle. Once that limit is reached no additional clients can register subscriptions.

Ok no problem, just scale horizontally, spin up additional ASP.NET server instances, add a load balancer and have the new requests open a web socket connection to these additional server instances, right? Not so fast.
Ok no problem, just scale horizontally, spin up additional ASP.NET server instances, add a load balancer and have the new requests open a web socket connection to these additional server instances, right? Not so fast.

With the examples above events published by any mutation using `PublishSubscriptionEvent` are routed internally directly to the local subscription server meaning only those clients connected to the server where the event was raised will receive it. Clients connected to other server instances will never know an event was raised. This represents a big problem for large scale websites, so what do we do?

### Custom Event Publishing
Instead of publishing events internally, within the server instance, we need to publish our events to some intermediate source such that any server can be notified of the change. There are a variety of technologies to handle this scenario; be it a common database or messaging technologies like RabbitMQ, Azure Service Bus etc.

Instead of publishing events internally, within the server instance, we need to publish our events to some intermediate source such that any server can be notified of the change. There are a variety of technologies to handle this scenario; be it a common database or messaging technologies like RabbitMQ, Azure Service Bus etc.

#### Implement `ISubscriptionEventPublisher`

Whatever your technology of choice the first step is to create and register a custom publisher. How your individual class functions will vary widely depending on your implementation.

```C#
Expand All @@ -166,6 +176,7 @@ Whatever your technology of choice the first step is to create and register a cu
```

Register your publisher with the DI container BEFORE calling `.AddGraphQL()`

```C#
// startup.cs

Expand All @@ -185,7 +196,8 @@ Register your publisher with the DI container BEFORE calling `.AddGraphQL()`
Publishing your SubscriptionEvents externally is not trivial. You'll have to deal with concerns like data serialization, package size etc..

### Consuming Published Events
At this point, we've successfully published our events to some external data source. Now we need to consume them. How that occurs is, again, implementation specific. Perhaps you run a background hosted service to watch for messages on an Azure Service Bus topic or perhaps you periodically pole a database table to look for new events. The ways in which data may be shared is endless.

At this point, we've successfully published our events to some external data source. Now we need to consume them. How that occurs is, again, implementation specific. Perhaps you run a background hosted service to watch for messages on an Azure Service Bus topic or perhaps you periodically pole a database table to look for new events. The ways in which data may be shared is endless.

Once you rematerialize a `SubscriptionEvent` you need to let GraphQL know that it occurred. this is done using the `ISubscriptionEventRouter`. In general, you won't need to implement your own router, just inject it into your listener service then call `RaiseEvent` and GraphQL will take it from there.

Expand All @@ -211,17 +223,18 @@ Once you rematerialize a `SubscriptionEvent` you need to let GraphQL know that i
}
}
```

The router will take care of figuring out which schema the event is destined for, which local subscription servers are registered to receive that event and forward the data as necessary for processing.

### Azure Service Bus Example

A complete example of a scalable subscription configuration including serialization and deserialization using the Azure Service Bus is available in the [demo projects](../reference/demo-projects) section.


## Subscription Server Configuration
>See [schema configuration](../reference/schema-configuration#subscription-server-options) for information on individual subscription server configuration options.

Currently, when using the `.AddSubscriptions()` extension method two seperate operations occur:
> See [schema configuration](../reference/schema-configuration#subscription-server-options) for information on individual subscription server configuration options.

Currently, when using the `.AddSubscriptions()` extension method two seperate operations occur:

1. The subscription server components are registered to the DI container, the graphql execution pipeline is modified to support registering subscriptions and a middleware component is appended to the ASP.NET pipeline to intercept web sockets and forward client connections to the the subscription server component.

Expand All @@ -231,19 +244,29 @@ Some applications may wish to split these operations in different server instanc

The following more granular configuration options are available:

* `.AddSubscriptionServer()` :: Only configures the ASP.NET pipeline to intercept websockets and adds the subscription server components to the DI container.

* `.AddSubscriptionPublishing()` :: Only configures the graphql execution pipeline and the `ISubscriptionEventPublisher`. Subscription registration and Websocket support is **NOT** enabled.
- `.AddSubscriptionServer()` :: Only configures the ASP.NET pipeline to intercept websockets and adds the subscription server components to the DI container.

- `.AddSubscriptionPublishing()` :: Only configures the graphql execution pipeline and the `ISubscriptionEventPublisher`. Subscription registration and Websocket support is **NOT** enabled.

## Security & Query Authorization

Because subscriptions are long running and registered before any data is processed, the subscription server requires a [query authorization method](../reference/schema-configuration#authorization-options) of `PerRequest`. This allows the subscription query to be fully validated before its registered with the server. This authorization method is set globally at startup and will apply to queries and mutations as well.

This is different than the default behavior when subscriptions are not enabled. Queries and mutations, by default, will follow a `PerField` method allowing for partial query resolutions.
This is different than the default behavior when subscriptions are not enabled. Queries and mutations, by default, will follow a `PerField` method allowing for partial query resolutions.

**Note:** Allowing `PerField` authorization for subscriptions is slated for a future release.

## Query Timeouts

By default GraphQL does not define a timeout for an executed query. The query will run as long as the underlying HTTP connection is open. This is true for subscriptions as well. Given that the websocket connection is never closed while the end user is connected, any query executed through the websocket will be allowed to run for an infinite amount of time which can have some unintended side effects and consume resources unecessarily.

Optionally, you can define a query timeout for a given schema, which the subscription server will obey:

```csharp
// startup.cs
services.AddGraphQL(o =>
{
// define a 2 minute timeout per query executed.
o.ExecutionOptions.QueryTimeout = TimeSpan.FromMinutes(2);
})
```
46 changes: 41 additions & 5 deletions docs/controllers/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ public class BakeryController : GraphController
// schema syntax: [Donut]
[Mutation("donutsAsAnArray")]
public bool DonutsAsAnArray(Donut[] donuts)
{/*....*/}
{/*....*/}

// This is a valid nested list
// schema syntax: [[[Donut]]]
Expand Down Expand Up @@ -562,7 +562,7 @@ public class BakeryController : GraphController

```javascript
query {
searchDonuts(searchParams:
searchDonuts(searchParams:
name: "jelly*"
filled: true
dayOld: false){
Expand All @@ -580,7 +580,7 @@ At runtime, GraphQL will try to validate every parameter passed on a query again

One might think, well it should be passed as an object reference to the dictionary parameter:

```javascript
```ruby
query {
searchDonuts( searchParams : {name: "jelly*" filled: true dayOld: false }){
id
Expand Down Expand Up @@ -608,8 +608,6 @@ public class DonutSearchParams
// BakeryController.cs
public class BakeryController : GraphController
{
// ERROR, a GraphDeclarationException
// will be thrown.
[QueryRoot]
public IEnumerable<Donut>
SearchDonuts(DonutSearchParams searchParams)
Expand All @@ -626,3 +624,41 @@ query {
```

</div>

## Cancellation Tokens

As with REST based ASP.NET action methods, your graph controller action methods can accept an optional cancellation token. This is useful when doing some long running activities such as IO, database queries, API orchestration etc. To make use of a cancellation token simply add it as a parameter to your method. GraphQL will automatically wire up the token for you:

```csharp
// BakeryController.cs
public class BakeryController : GraphController
{
// Add a CancellationToken to your controller method
[QueryRoot(typeof(IEnumerable<Donut>))]
public async Task<IGraphActionResult> SearchDonuts(string name, CancellationToken cancelToken)
{/* ... */}
}
```

> Depending on your usage of the cancellation token a `TaskCanceledException` may be thrown. GraphQL will not attempt to intercept this exception and will log it as an error-level, unhandled exception if allowed to propegate. The query will still be cancelled as expected.

### Defining a Query Timeout

By default GraphQL does not define a timeout for an executed query. The query will run as long as the underlying HTTP connection is open. In fact, the `CancellationToken` passed to your action methods is the same Cancellation Token offered on the HttpContext when it receives the initial post request.

Optionally, you can define a query timeout for a given schema:

```csharp
// startup.cs
services.AddGraphQL(o =>
{
// define a 2 minute timeout per query executed.
o.ExecutionOptions.QueryTimeout = TimeSpan.FromMinutes(2);
})
```

When a timeout is defined, the token passed to your action methods is a combined token representing the HttpContext as well as the timeout operation. That is to say the token will indicate a cancellation if the alloted query time expires or the http connection is closed which ever comes first. When the timeout expires the caller will receive a response indicating the timeout. However, if the its the HTTP connection that is closed, the operation is simply halted and no result is produced.

### Timeouts and Subscriptions

The same rules for cancellation tokens apply to subscriptions as well. Since the websocket connection is a long running operation it will never be closed until the connection is closed. To prevent some processes from spinning out of control its a good idea to define a query timeout when implementing a subscription server. This way, even though the connection remains open the query will terminate and release resources if something goes awry.
4 changes: 2 additions & 2 deletions docs/reference/schema-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ schemaOptions.ExecutionOptions.QueryTimeout = TimeSpan.FromMinutes(2);

| Default Value | Acceptable Values |
| ------------- | -------------------------- |
| 1 Minute | Minimum of 10 milliseconds |
| null | Minimum of 10 milliseconds |

The amount of time an individual query will be given to run to completion before being abandoned and canceled by the runtime.
The amount of time an individual query will be given to run before being abandoned and canceled by the runtime. By default, the timeout is disabled and a query will continue to execute as long as the underlying HTTP request is also executing.

### DebugMode

Expand Down
2 changes: 1 addition & 1 deletion website/pages/en/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class HomeSplash extends React.Component {
<h2 className="projectTitle">
{<span>GraphQL ASP.NET</span>}
{/*<small>{siteConfig.tagline}</small>*/}
<small>v0.12.0-beta</small>
<small>v0.12.1-beta</small>
</h2>
);

Expand Down