You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* Added language about how to use cancellation tokens
* Additional updates to action results, actions, input object docs
* Updated query timeout defaults
Copy file name to clipboardExpand all lines: docs/advanced/subscriptions.md
+37-14Lines changed: 37 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,9 +6,10 @@ sidebar_label: Subscriptions
6
6
7
7
## Initial Setup
8
8
9
-
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.
9
+
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.
10
10
11
11
### Install the Subscriptions Package
12
+
12
13
The first step to using subscriptions is to install the subscription server package.
13
14
14
15
```Powershell
@@ -23,7 +24,6 @@ You must configure web socket support for your Asp.Net server instance separatel
23
24
24
25
After web sockets are added to your server, add subscription support to the graphql registration.
25
26
26
-
27
27
```C#
28
28
// startup.cs
29
29
@@ -47,6 +47,7 @@ After web sockets are added to your server, add subscription support to the grap
47
47
> Don't forget to add `UseWebsockets` in the `Configure` method of startup.cs
48
48
49
49
### Create a Subscription
50
+
50
51
Declaring a subscription is the same as declaring a query or mutation on a controller but with `[Subscription]` and `[SubscriptionRoot]` attributes.
51
52
52
53
```C#
@@ -63,11 +64,13 @@ public class SubscriptionController : GraphController
63
64
}
64
65
}
65
66
```
67
+
66
68
> Subscriptions can be asyncronous and return a Task<IGraphActionResult> as well.
67
69
68
70
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.
69
71
70
72
A query to invoke this subscription may look like this:
73
+
71
74
```javascript
72
75
subscription {
73
76
onWidgetChanged(filter:"Big"){
@@ -77,12 +80,13 @@ subscription {
77
80
}
78
81
}
79
82
```
83
+
80
84
Any updated widgets that start with the phrase "Big" will then be sent to the requestor as they are changed on the server.
85
+
81
86
### Publish a Subscription Event
82
87
83
88
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.
84
89
85
-
86
90
```C#
87
91
publicclassMutationController : GraphController
88
92
{
@@ -106,6 +110,7 @@ public class MutationController : GraphController
106
110
> 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.
107
111
108
112
### Subscription Event Source Data
113
+
109
114
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.
110
115
111
116
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.
@@ -127,11 +132,13 @@ public class SubscriptionController : GraphController
127
132
}
128
133
}
129
134
```
130
-
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.
131
135
136
+
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.
132
137
133
138
### Summary
139
+
134
140
That's all there is for a basic subscription server setup.
141
+
135
142
1. Add the package reference and update startup.cs
136
143
2. Create a new subscription using `[Subscription]` or `[SubscriptionRoot]`
137
144
3. Publish an event from a mutation
@@ -141,16 +148,19 @@ That's all there is for a basic subscription server setup.
141
148
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.
142
149
143
150
## Scaling Subscription Servers
151
+
144
152
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.
145
153
146
-
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.
154
+
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.
147
155
148
156
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?
149
157
150
158
### Custom Event Publishing
151
-
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.
159
+
160
+
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.
152
161
153
162
#### Implement `ISubscriptionEventPublisher`
163
+
154
164
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.
155
165
156
166
```C#
@@ -166,6 +176,7 @@ Whatever your technology of choice the first step is to create and register a cu
166
176
```
167
177
168
178
Register your publisher with the DI container BEFORE calling `.AddGraphQL()`
179
+
169
180
```C#
170
181
// startup.cs
171
182
@@ -185,7 +196,8 @@ Register your publisher with the DI container BEFORE calling `.AddGraphQL()`
185
196
Publishing your SubscriptionEvents externally is not trivial. You'll have to deal with concerns like data serialization, package size etc..
186
197
187
198
### Consuming Published Events
188
-
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.
199
+
200
+
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.
189
201
190
202
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.
191
203
@@ -211,17 +223,18 @@ Once you rematerialize a `SubscriptionEvent` you need to let GraphQL know that i
211
223
}
212
224
}
213
225
```
226
+
214
227
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.
215
228
216
229
### Azure Service Bus Example
217
230
218
231
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.
219
232
220
-
221
233
## Subscription Server Configuration
222
-
>See [schema configuration](../reference/schema-configuration#subscription-server-options) for information on individual subscription server configuration options.
223
234
224
-
Currently, when using the `.AddSubscriptions()` extension method two seperate operations occur:
235
+
> See [schema configuration](../reference/schema-configuration#subscription-server-options) for information on individual subscription server configuration options.
236
+
237
+
Currently, when using the `.AddSubscriptions()` extension method two seperate operations occur:
225
238
226
239
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.
227
240
@@ -231,19 +244,29 @@ Some applications may wish to split these operations in different server instanc
231
244
232
245
The following more granular configuration options are available:
233
246
234
-
*`.AddSubscriptionServer()` :: Only configures the ASP.NET pipeline to intercept websockets and adds the subscription server components to the DI container.
235
-
236
-
*`.AddSubscriptionPublishing()` :: Only configures the graphql execution pipeline and the `ISubscriptionEventPublisher`. Subscription registration and Websocket support is **NOT** enabled.
247
+
-`.AddSubscriptionServer()` :: Only configures the ASP.NET pipeline to intercept websockets and adds the subscription server components to the DI container.
237
248
249
+
-`.AddSubscriptionPublishing()` :: Only configures the graphql execution pipeline and the `ISubscriptionEventPublisher`. Subscription registration and Websocket support is **NOT** enabled.
238
250
239
251
## Security & Query Authorization
240
252
241
253
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.
242
254
243
-
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.
255
+
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.
244
256
245
257
**Note:** Allowing `PerField` authorization for subscriptions is slated for a future release.
246
258
259
+
## Query Timeouts
247
260
261
+
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.
248
262
263
+
Optionally, you can define a query timeout for a given schema, which the subscription server will obey:
@@ -608,8 +608,6 @@ public class DonutSearchParams
608
608
// BakeryController.cs
609
609
publicclassBakeryController : GraphController
610
610
{
611
-
// ERROR, a GraphDeclarationException
612
-
// will be thrown.
613
611
[QueryRoot]
614
612
public IEnumerable<Donut>
615
613
SearchDonuts(DonutSearchParamssearchParams)
@@ -626,3 +624,41 @@ query {
626
624
```
627
625
628
626
</div>
627
+
628
+
## Cancellation Tokens
629
+
630
+
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:
631
+
632
+
```csharp
633
+
// BakeryController.cs
634
+
publicclassBakeryController : GraphController
635
+
{
636
+
// Add a CancellationToken to your controller method
> 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.
644
+
645
+
### Defining a Query Timeout
646
+
647
+
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.
648
+
649
+
Optionally, you can define a query timeout for a given schema:
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.
661
+
662
+
### Timeouts and Subscriptions
663
+
664
+
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.
The amount of time an individual query will be given to run to completion before being abandoned and canceled by the runtime.
180
+
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.
0 commit comments