diff --git a/docs/advanced/directives.md b/docs/advanced/directives.md index b1300ff..57218d3 100644 --- a/docs/advanced/directives.md +++ b/docs/advanced/directives.md @@ -4,16 +4,14 @@ title: Directives sidebar_label: Directives --- -> Directives were completely reimagined in August 2022, this document represents the new approach to directives. - ## What is a directive? -Directives decorate, or are attached to, parts of your schema or query document to perform some sort of custom logic. What that logic is, is entirely up to you. There are several built in directives: +Directives decorate, or are attached to, parts of your schema or query document to perform some sort of custom logic. What that logic is, is entirely up to you. There are several built in directives: -- `@include` : An execution directive that conditionally includes a field or fragment in the results of a graphql query -- `@skip` : An execution directive conditionally excludes a field or fragment from the results of a graphql query -- `@deprecated` : A type system directive that marks a field definition or enum value as deprecated, indicating that it may be removed in a future release of your graph. -- `@specifiedBy` : A type system directive for a custom scalar that adds a URL pointing to documentation about how the scalar is used. This url is returned as part of an introspection query. +- `@include` : An execution directive that conditionally includes a field or fragment in the results of a graphql query +- `@skip` : An execution directive that conditionally excludes a field or fragment from the results of a graphql query +- `@deprecated` : A type system directive that marks a field definition or enum value as deprecated, indicating that it may be removed in a future release of your graph. +- `@specifiedBy` : A type system directive for a custom scalar that adds a URL pointing to documentation about how the scalar is used. This url is returned as part of an introspection query. Beyond this you can create directives to perform any sort of action against your graph or query document as seems fit to your use case. @@ -24,12 +22,12 @@ Directives are implemented in much the same way as a `GraphController` but where ```csharp // an example implementation of the @skip directive public sealed class SkipDirective : GraphDirective - { + { [DirectiveLocations(DirectiveLocation.FIELD | DirectiveLocation.FRAGMENT_SPREAD | DirectiveLocation.INLINE_FRAGMENT)] public IGraphActionResult Execute([FromGraphQL("if")] bool ifArgument) { - if (this.DirectiveTarget is IIncludeableDocumentPart rdp) - rdp.IsIncluded = !ifArgument; + if (this.DirectiveTarget is IIncludeableDocumentPart docPart) + docPart.IsIncluded = !ifArgument; return this.Ok(); } @@ -44,35 +42,35 @@ All directives must: All directive action methods must: - Share the same method signature - - The return type must match exactly - - The input arguments must match exactly in name, casing and declaration order. + - The return type must match exactly + - The input arguments must match exactly in name, casing and declaration order. - Return a `IGraphActionResult` or `Task` ### Action Results + Directives have two built in action results that can be returned: -* `this.Ok()` - * Indicates that the directive completed successfully and processing should continue. -* `this.Cancel()` - * Indicates that the directive did NOT complete successfully and processing should stop. - * If this is a type system directive, the target schema will not be generated and the server will fail to start. - * If this is an execution directive, the query will be abandoned and the caller will receive an error result. +- `this.Ok()` + - Indicates that the directive completed successfully and processing should continue. +- `this.Cancel()` + - Indicates that the directive did NOT complete successfully and processing should stop. + - If this is a type system directive, the target schema will not be generated and the server will fail to start. + - If this is an execution directive, the query will be abandoned and the caller will receive an error result. ### Helpful Properties The following properties are available to all directive action methods: -* `this.DirectiveTarget` - The `ISchemaItem` or `IDocumentPart` to which the directive is being applied. -* `this.Request` - The directive invocation request for the currently executing directive. Contains lots of advanced information just as execution phase, the directive type declared on the schema etc. - +- `this.DirectiveTarget` - The `ISchemaItem` or `IDocumentPart` to which the directive is being applied. +- `this.Request` - The directive invocation request for the currently executing directive. Contains lots of advanced information such as execution phase, the directive type declared on the schema etc. ### Directive Arguments -Directives may contain input arguments just like fields. However, its important to note that while a directive may declare multiple action methods for different locations to seperate your logic better, it is only a single entity in the schema. As a result, ALL action methods must share a common signature. The runtime will throw an exception while creating your schema if the signatures of each action method differ. +Directives may contain input arguments just like fields. However, its important to note that while a directive may declare multiple action methods for different locations to seperate your logic better, it is only a single entity in the schema. As a result, ALL action methods must share a common signature. The runtime will throw an exception while creating your schema if the signatures of the action methods differ. ```csharp public class MyValidDirective : GraphDirective - { + { [DirectiveLocations(DirectiveLocation.FIELD)] public IGraphActionResult ExecuteField(int arg1, string arg2) { /.../ } @@ -81,7 +79,7 @@ Directives may contain input arguments just like fields. However, its important } public class MyInvalidDirective : GraphDirective - { + { [DirectiveLocations(DirectiveLocation.FIELD)] public IGraphActionResult ExecuteField(int arg1, int arg2) { /.../ } @@ -90,10 +88,12 @@ Directives may contain input arguments just like fields. However, its important public IGraphActionResult ExecuteFragSpread(int arg1, string arg2) { /.../ } } ``` + > Directive arguments must match in name, data type and position for all action methods. Being able to use different methods for different locations is a convenience; to GraphQL there is only one directive with one set of parameters. ### Directive Target -The `DirectiveTarget` property available to your directive action methods will contain either an `ISchemaItem` for type system directives or an `IDocumentPart` for execution directives. + +The `DirectiveTarget` property available to your directive will contain either an `ISchemaItem` for type system directives or an `IDocumentPart` for execution directives. ## Execution Directives @@ -104,7 +104,7 @@ This is the code for the built in `@include` directive: ```csharp [GraphType("include")] public sealed class IncludeDirective : GraphDirective - { + { [DirectiveLocations(DirectiveLocation.FIELD | DirectiveLocation.FRAGMENT_SPREAD | DirectiveLocation.INLINE_FRAGMENT)] public IGraphActionResult Execute([FromGraphQL("if")] bool ifArgument) { @@ -122,7 +122,7 @@ This Directive: - The name will be derived from the class name if the attribute is omitted - Declares that it can be applied to a query document at all field selection locations using the `[DirectiveLocations]` attribute - Uses the `[FromGraphQL]` attribute to declare the input argument's name in the schema - - This is because `if` is a keyword in C# and we don't want the argument being named `ifArgument` in the graph. + - This is because `if` is a keyword in C# and we don't want the argument being named `ifArgument` in the graph. - Is executed once for each field, fragment spread or inline fragment to which its applied in a query document. > The action method name `Execute` in this example is arbitrary. Method names can be whatever makes the most sense to you. @@ -144,11 +144,13 @@ query { } ``` -The directives attached to the `id` field are executed in order: - 1. @directiveA - 2. @directiveB +The directives attached to the `id` field are executed in order from left to right: + +1. @directiveA +2. @directiveB ### Influencing Field Resolution + Execution directives are applied to document parts, not schema items. As a result they aren't directly involved in resolving fields but instead influence the document that is eventually translated into a query plan and executed. However, one common use case for execution directives includes augmenting the results of a field after its resolved. For instance, perhaps you had a directive that could conditionally turn a string field into an upper case string when applied (i.e. `@toUpper`). For this reason it is possible to apply a 'PostResolver' directly to an `IFieldDocumentPart`. This post resolver is executed immediately after the primary field resolver is executed. @@ -161,12 +163,12 @@ For this reason it is possible to apply a 'PostResolver' directly to an `IFieldD { if (this.DirectiveTarget as IFieldDocumentPart fieldPart) { - // + // if (fieldPart.Field?.ObjectType != typeof(string)) throw new GraphExecutionException("ONLY STRINGS!"); // - hulk - // add a post resolver to the target field document - // part to perform the conversion when the query is + // add a post resolver to the target field document + // part to perform the conversion when the query is // ran fieldPart.PostResolver = ConvertToUpper; } @@ -175,7 +177,7 @@ For this reason it is possible to apply a 'PostResolver' directly to an `IFieldD } private static Task ConvertToUpper( - FieldResolutionContext context, + FieldResolutionContext context, CancellationToken token) { if (context.Result is string) @@ -188,15 +190,17 @@ For this reason it is possible to apply a 'PostResolver' directly to an `IFieldD ``` #### Working with Batch Extensions -Batch extensions work differently than standard field resolvers; they don't resolve a single item at a time. This means our `@toUpper` example above won't work as `context.Result` won't be a string. Should you employ a post resolver that may be applied to a batch extension you'll need to handle the resultant dictionary differently than you would a single field value. The dictionary will always be of the format `IDictionary` where `TSource` is the data type of the field set that owns the field the directive was applied to and `TResult` is the data type or an `IEnumerable` of the data type for the field, depending on the + +Batch extensions work differently than standard field resolvers; they don't resolve a single item at a time. This means our `@toUpper` example above won't work as `context.Result` won't be a string. Should you employ a post resolver that may be applied to a batch extension you'll need to handle the resultant dictionary differently than you would a single field value. The dictionary will always be of the format `IDictionary` where `TSource` is the data type of the field set that owns the field the directive was applied to and `TResult` is the data type or an `IEnumerable` of the data type for the field, depending on the batch extension declaration. The dictionary is always keyed by source item reference. > Batch Extensions will return a dictionary of data not a single item. Your post resolver must be able to handle this dictionary if applied to a field that is a `[BatchExtensionType]`. ## Type System Directives + ### Example: @toLower -This directive will extend the resolver of a field, as its declared in the schema, to turn any strings into lower case letters. +This directive will extend the resolver of a field, as its declared in the schema, to turn any strings into lower case letters. ```csharp public class ToLowerDirective : GraphDirective @@ -230,20 +234,20 @@ This directive will extend the resolver of a field, as its declared in the schem } ``` -This Directive: +This Directive: -* Targets a FIELD_DEFINITION. -* Ensures that the target field returns a string. -* Extends the field's resolver to convert the result to an upper case string. -* The directive is executed once per field definition its applied to when the schema is created. The extension method is executed on every field resolution. +- Targets a FIELD_DEFINITION. +- Ensures that the target field returns a string. +- Extends the field's resolver to convert the result to an upper case string. +- The directive is executed once per field definition its applied to when the schema is created. The extension method is executed on every field resolution. +> Notice the difference in this type system directive vs. the `@toUpper` execution directive above. Where as toUpper was declared as a PostResolver on the document part, this directive extends the primary resolver of an `IGraphField` and affects ALL queries that request this field. ->Notice the difference in this type system directive vs. the `@toUpper` execution directive above. Where as toUpper was declared as a PostResolver on the document part, this directive extends the primary resolver of an `IGraphField` and affects ALL queries that request this field. ### Example: @deprecated The `@deprecated` directive is a built in type system directive provided by graphql to indicate deprecation on a field definition or enum value. Below is the code for its implementation. -```csharp +```csharp public sealed class DeprecatedDirective : GraphDirective { [DirectiveLocations(DirectiveLocation.FIELD_DEFINITION | DirectiveLocation.ENUM_VALUE)] @@ -265,12 +269,11 @@ The `@deprecated` directive is a built in type system directive provided by grap } ``` +This Directive: -This Directive: - -* Targets a FIELD_DEFINITION or ENUM_VALUE. -* Marks the field or enum value as deprecated and attaches the provided deprecation reason -* The directive is executed once per field definition and enum value its applied to when the schema is created. +- Targets a FIELD_DEFINITION or ENUM_VALUE. +- Marks the field or enum value as deprecated and attaches the provided deprecation reason +- The directive is executed once per field definition and enum value its applied to when the schema is created. ### Applying Type System Directives @@ -283,7 +286,7 @@ If you have access to the source code of a given type you can use the `[ApplyDir ```csharp // Person.cs -public class Person +public class Person { [ApplyDirective(typeof(ToLowerDirective))] public string Name{ get; set; } @@ -306,14 +309,13 @@ type Person {
If different schemas on your server will use different implementations of the directive you can also specify the directive by name. This name is case sensitive and must match the name of the registered directive in the target schema. At runtime, the concrete class declared as the directive in each schema will be instantiated and used. -
```csharp // Person.cs [ApplyDirective("monitor")] -public class Person +public class Person { public string Name{ get; set; } } @@ -343,10 +345,10 @@ Arguments added to the apply directive attribute will be passed to the directive ```csharp // Person.cs -public class Person +public class Person { [ApplyDirective( - "deprecated", + "deprecated", "Names don't matter")] public string Name{ get; set; } } @@ -386,7 +388,7 @@ public void ConfigureServices(IServiceCollection services) // mark Person.Name as deprecated options.ApplyDirective("monitor") - .ToItems(schemaItem => + .ToItems(schemaItem => schemaItem.IsObjectGraphType()); } } @@ -407,7 +409,7 @@ type Person @monitor {
-> The `ToItems` filter can be invoked multiple times. A schema item must match all filter criteria in order for the directive to be applied. +> The `ToItems` filter can be invoked multiple times. A schema item must match all filter criteria in order for the directive to be applied. > Type system directives are applied in order of declaration with the `[ApplyDirective]` attributes taking precedence over the `.ApplyDirective()` method. @@ -416,7 +418,6 @@ type Person @monitor { Adding Arguments via schema options is a lot more flexible than via attributes. Use the `.WithArguments` method to supply either a static set of arguments for all matched schema items or a `Func` that returns a collection of any parameters you want on a per item basis. -
@@ -431,11 +432,12 @@ public void ConfigureServices(IServiceCollection services) options.AddGraphType(); options.ApplyDirective("deprecated") .WithArguments("Names don't matter") - .ToItems(schemaItem => + .ToItems(schemaItem => schemaItem.IsGraphField("name")); } } ``` +
@@ -455,10 +457,10 @@ type Person { GraphQL ASP.NET supports repeatable type system directives. Sometimes it can be helpful to apply your directive to an schema item more than once, especially if you supply different parameters on each application. -Add the `[Repeatable]` attribute to the directive definition and you can the apply it multiple times using the standard methods. GraphQL tools that support this the repeatable syntax +Add the `[Repeatable]` attribute to the directive definition and you can the apply it multiple times using the standard methods. GraphQL tools that support this the repeatable syntax will be able to properly interprete your schema. -```csharp +```csharp // apply repeatable attribute [Repeatable] public sealed class ScanItemDirective : GraphDirective @@ -488,30 +490,33 @@ services.AddGraphQL(o => { ``` ### Understanding the Type System -GraphQL ASP.NET builds your schema and all of its types from your controllers and objects. In general, this is done behind the scenes and you do not need to interact with it. However, when applying type system directives you are affecting the generated schema and need to understand the various parts of it. If you have a question don't be afraid to ask on [github](https://github.com/graphql-aspnet/graphql-aspnet). + +GraphQL ASP.NET builds your schema and all of its types from your controllers and objects. In general, this is done behind the scenes and you do not need to interact with it. However, when applying type system directives you are affecting the generated schema and need to understand the various parts of it. If you have a question don't be afraid to ask on [github](https://github.com/graphql-aspnet/graphql-aspnet). **UML Diagrams** -These [uml diagrams](../assets/2022-05-graphql-aspnet-type-system-interface-diagrams.pdf) detail the major interfaces and their most useful properties of the type system. However, -these diagrams are not exaustive. Look at the [source code](https://github.com/graphql-aspnet/graphql-aspnet/tree/master/src/graphql-aspnet/Interfaces/TypeSystem) for the full definitions. +These [uml diagrams](../assets/2022-10-graphql-aspnet-structural-diagrams.pdf) detail the major interfaces and their most useful properties of the type system. However, these diagrams are not exaustive. Look at the [source code](https://github.com/graphql-aspnet/graphql-aspnet/tree/master/src/graphql-aspnet/Interfaces/TypeSystem) for the full definitions. **Helpful Extensions** -There are a robust set of of built in extensions for `ISchemaItem` that can help you filter your data when applying directives. See the [full source code](https://github.com/graphql-aspnet/graphql-aspnet/tree/master/src/graphql-aspnet/Configuration/SchemaItemExtensions.cs) for details. +There are a robust set of of built in extensions for `ISchemaItem` that can help you filter your data when applying directives. See the [full source code](https://github.com/graphql-aspnet/graphql-aspnet/blob/master/src/graphql-aspnet/Configuration/SchemaItemFilterExtensions.cs) for details. ## Directives as Services -Directives are invoked as services through your DI container when they are executed. When you add types to your schema during its initial configuration, GraphQL ASP.NET will automatically register any directives it finds attached to your entities as services in your `IServiceCollection` instance. However, there are times when it cannot do this, such as when you apply a directive by its string declared name. These late-bound directives may still be discoverable later and graphql will attempt to add them to your schema whenever it can. However, it may do this after the opportunity to register them with the DI container has passed. -When this occurs, if your directive contains a public, parameterless constructor graphql will still instantiate and use your directive as normal. If the directive contains dependencies in the constructor that it can't resolve, execution of that directive will fail and an exception will be thrown. To be safe, make sure to add any directives you may use to your schema during the `.AddGraphQL()` configuration method. Directives are directly discoverable and will be included via the `options.AddAssembly()` helper method as well. +Directives are invoked as services through your DI container when they are executed. When you add types to your schema during its initial configuration, GraphQL ASP.NET will automatically register any directives it finds attached to your entities as services in your `IServiceCollection` instance. However, there are times when it cannot do this, such as when you apply a directive by its string declared name. These late-bound directives may still be discoverable later and graphql will attempt to add them to your schema whenever it can. However, it may do this after the opportunity to register them with the DI container has passed. + +When this occurs, if your directive contains a public, parameterless constructor graphql will still instantiate and use your directive as normal. If the directive contains dependencies in the constructor that it can't resolve, execution of that directive will fail and an exception will be thrown. To be safe, make sure to add any directives you may use to your schema during the `.AddGraphQL()` configuration method. Directives are directly discoverable and will be included via the `options.AddAssembly()` helper method as well. The benefit of ensuring your directives are part of your `IServiceCollection` should be apparent: -* The directive instance will obey lifetime scopes (e.g. transient, scoped, singleton). -* The directive can be instantiated with any dependencies or services you wish; making for a much richer experience. + +- The directive instance will obey lifetime scopes (e.g. transient, scoped, singleton). +- The directive can be instantiated with any dependencies or services you wish; making for a much richer experience. ## Directive Security + Directives can be secured like controller actions. However, where a controller action represents a field in the graph, a directive action does not. Regardless of the number of action methods, there is only one directive definition in your schema. As a result, the directive is secured at the class level not the method level. Any applied security parameters effect ALL action methods equally. -Take for example that the graph schema included a field of data that, by default, was always rendered in a redacted state (meaning it was obsecured) such as social security number. You could have a directive that, when supplied by the requestor, would unredact the field and allow the value to be displayed. +Take for example that the graph schema included a field of data that, by default, was always rendered in a redacted state (meaning it was obsecured) such as social security number. You could have a directive that, when supplied by the requestor, would unredact the field and allow the value to be displayed. ``` [Authorize(Policy = "admin")] @@ -523,16 +528,17 @@ public sealed class UnRedactDirective : GraphDirective } ``` -> A user must adhere to the requirements of the `admin` policy in order to apply the `@unRedact` directive to a field. If the user is not part of this policy and they attempt to apply the directive, the query will be rejected. +> A user must adhere to the requirements of the `admin` policy in order to apply the `@unRedact` directive to a field. If the user is not part of this policy and they attempt to apply the directive, the query will be rejected. -### Security Scenarios +### Security Scenarios -* **Execution Directives** - These directives execute using the same security context and `ClaimsPrincipal` applied to the HTTP request; such as an oAuth token. Execution directives are evaluated against the source document while its being constructed, BEFORE it is executed. As a result, if an execution directive fails authorization, the document fails to be constructed and no fields are resolved. This is true regardless of the authorization method assigned to the schema. +- **Execution Directives** - These directives execute using the same security context and `ClaimsPrincipal` applied to the HTTP request; such as an oAuth token. Execution directives are evaluated against the source document while its being constructed, BEFORE it is executed. As a result, if an execution directive fails authorization, the document fails to be constructed and no fields are resolved. This is true regardless of the authorization method assigned to the schema. -* **Type System Directives** - These directives are executed during server startup, WITHOUT a `ClaimsPrincipal`, while the schema is being built. As a result, type system directives should not contain any security requirements, they will fail to execute if any security parameters are defined. +- **Type System Directives** - These directives are executed during server startup, WITHOUT a `ClaimsPrincipal`, while the schema is being built. As a result, type system directives should not contain any security requirements, they will fail to execute if any security parameters are defined. > Since type system directives execute outside of a specific user context, only apply type system directives that you trust. ## Demo Project + See the [Demo Projects](../reference/demo-projects.md) page for a demonstration on creating a type system directive for extending a field resolver and an execution directives - that manipulates a string field result at runtime. \ No newline at end of file +that manipulates a string field result at runtime. diff --git a/docs/advanced/subscriptions.md b/docs/advanced/subscriptions.md index aff7ba8..0cd623c 100644 --- a/docs/advanced/subscriptions.md +++ b/docs/advanced/subscriptions.md @@ -20,7 +20,7 @@ This adds the necessary components to create a subscription server for a given s ### Configure the Server Instance -You must configure web socket support for your Asp.Net server instance separately. The ways in which you perform this configuration will vary widely depending on your needs. CORS requirements, keep-alive support etc. will be different for each scenario. +You must configure web socket support for your Asp.Net server instance separately. The ways in which you perform this configuration will vary widely depending on your CORS requirements, keep-alive support and other needs. After web sockets are added to your server, add subscription support to the graphql registration. @@ -54,13 +54,22 @@ Declaring a subscription is the same as declaring a query or mutation on a contr public class SubscriptionController : GraphController { // other code not shown for brevity - // EventName will default to the subscription field name + + // EventName will default to the method name (i.e. "OnWidgetChanged") // if not supplied [SubscriptionRoot("onWidgetChanged", typeof(Widget), EventName = "WIDGET_CHANGED")] public IGraphActionResult OnWidgetChanged(Widget eventData, string filter){ if(eventData.Name.StartsWith(filter)) + { + // send the data down to the listening client return this.Ok(eventData); - return this.Ok(); + } + else + { + // use SkipSubscriptionEvent() to disregard the data + // and not communicate anything to the listening client + return this.SkipSubscriptionEvent(); + } } } ``` @@ -81,11 +90,11 @@ subscription { } ``` -Any updated widgets that start with the phrase "Big" will then be sent to the requestor as they are changed on the server. +Any updated widgets that start with the phrase "Big" will then be sent to the requestor as they are changed on the server. Any other changed widgets will be skipped/dropped and no data will be sent to the client. ### 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. +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, schema-unique keys that identify when something happened, usually via a mutation. Once the mutation publishes an event, the subscription server will execute the subscription method for any subscribers, using the supplied data, and deliver the results to the client. ```C# public class MutationController : GraphController @@ -107,11 +116,11 @@ 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. +> 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 +### Subscription Event Data Source -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. +In the example above, the data sent with `PublishSubscriptionEvent()` is the same as the first input parameter called `eventData` on the subscription field, which is the same as the field return type of the subscription controller method. By default, graphql will look for a parameter with the same data type as its return type and use that as the event data source. It will automatically populate this field with the data from `PublishSubscriptionEvent()` and this argument is not exposed in the object graph. 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. @@ -119,8 +128,7 @@ You can explicitly flag a different parameter, or a parameter of a different dat public class SubscriptionController : GraphController { // other code not shown for brevity - // EventName will default to the subscription field name - // if not supplied + [SubscriptionRoot("onWidgetChanged", typeof(Widget), EventName = "WIDGET_CHANGED")] public IGraphActionResult OnWidgetChanged( [SubscriptionSource] WidgetInternal eventData, @@ -128,13 +136,15 @@ public class SubscriptionController : GraphController { if(eventData.Name.StartsWith(filter)) return this.Ok(eventData.ToWidget()); - return this.Ok(); + return this.SkipSubscriptionEvent(); } } ``` 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. +> The data object published with `PublishSubscriptionEvent()` must have the same type as the `[SubscriptionSource]` on the subscription field. + ### Summary That's all there is for a basic subscription server setup. @@ -147,13 +157,26 @@ 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. +## Subscription Action Results + +You saw above the special action result `SkipSubscriptionEvent()` used to instruct graphql to skip this event and not tell the client about it; this can be very useful in scenarios where the subscription supplies filter data to only receive some very specific data and not all items published via a specific event. + +Here is a complete list of the various subscription action results: + +* `SkipSubscriptionEvent()` - Instructs the server to skip the raised event, no data will be sent to the client. +* `OkAndComplete(data)` - Works just like `this.Ok()` but ends the subscription after the event is completed. This action result does not close the underlying connection. + +> Plan your subscriptions carefully, you can quickly overload a server with only a small number of connected clients. + ## 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. -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? +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? + +[This diagram](../assets/2022-10-subscription-server.pdf) shows a high level differences between the default, single server configuration and a custom scalable solution. ### Custom Event Publishing @@ -161,7 +184,7 @@ Instead of publishing events internally, within the server instance, we need to #### 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. +Whatever your technology of choice the first step is to create and register a custom publisher that implements `ISubscriptionEventPublisher`. How your publisher class functions will vary widely depending on your implementation. ```C# public interface ISubscriptionEventPublisher @@ -199,9 +222,9 @@ Publishing your SubscriptionEvents externally is not trivial. You'll have to dea 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. +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 `RaisePublishedEvent` and GraphQL will take it from there. -```C# +```csharp public class MyListenerService : BackgroundService { private readonly ISubscriptionEventRouter _router; @@ -218,35 +241,36 @@ Once you rematerialize a `SubscriptionEvent` you need to let GraphQL know that i while (_notStopped) { SubscriptionEvent eventData = /* Fetch Next Event*/; - _router.RaiseEvent(eventData); + _router.RaisePublishedEvent(eventData); } } } ``` -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. +The router will take care of figuring out which schema the event is destined for, which clients have active subscriptions, 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. +A complete example of a bare bones example, including serialization and deserialization using the Azure Service Bus is available in the [demo projects](../reference/demo-projects) section. -## Subscription Server Configuration +> The demo project represents a functional starting point and lacks a lot of the error handling and resilency needs of a production environment. -> See [schema configuration](../reference/schema-configuration#subscription-server-options) for information on individual subscription server configuration options. +## Subscription Server Configuration 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. -2. A middleware component is appended to the end of the graphql execution pipeline to notify the `ISubscriptionEventPublisher` of any events raised by queries or mutations. +2. A middleware component is appended to the end of the graphql execution pipeline to formally publish any events staged via `PublishSubscriptionEvent()` Some applications may wish to split these operations in different server instances for handling load or just splitting different types of traffic. For example, having one set of servers dedicated to query/mutation operations (stateless requests) and others dedicated to handling subscriptions and websockets (stateful requests). -The following more granular configuration options are available: +The following more granular configuration options are available and may be useful for implementations where you maintain your querys/mutations separate from your websocket-enabled subscription servers: - `.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. +- `.AddSubscriptionPublishing()` :: Only configures the graphql execution pipeline to publish events. Subscription creation and websocket monitoring is **NOT** enabled. ## Security & Query Authorization @@ -266,7 +290,96 @@ Optionally, you can define a query timeout for a given schema, which the subscri // startup.cs services.AddGraphQL(o => { - // define a 2 minute timeout per query executed. + // define a 2 minute timeout per query or subscription event executed. o.ExecutionOptions.QueryTimeout = TimeSpan.FromMinutes(2); }) ``` + +## Websocket Protocols + +Out of the box, the library supports subscriptions over websockets using `graphql-transport-ws`, the modern protocol used by many client libraries as well as the legacy protocol `graphql-ws` (originally maintained by Apollo). A client requesting either protocol over a websocket will work with no additional configuration. + +### Supported Protocols + +- [graphql-transport-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) +- [graphql-ws](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) (_legacy_) + +### Creating Custom Protocols + +If you wish to add support for your own websocket messaging protocol you need to implement `ISubscriptionClientProxyFactory` and create instances +of a `ISubscriptionClientProxy` that can communicate with a connected client in your chosen protocol. + +```csharp + public interface ISubscriptionClientProxyFactory + { + // Use this factory to create a client proxy instance that + // acts as an intermediary to communicate server-side events + // to your client connection + Task> CreateClient(IClientConnection connection) + where TSchema : class, ISchema; + + // The unique name of the sub-protocol. A client requesting your + // protocol name will be handed to this + // factory to create the appropriate client proxy the server can + // communicate with. + string Protocol { get; } + } +``` + +And inject it into your DI container: + +```C# +// startup.cs +public void ConfigureServices(IServiceCollection services) +{ + services.AddSingleton(); + + services.AddGraphQL() + .AddSubscriptions(); +} +``` + +> `ISubscriptionClientProxyFactory` is expected to be a singleton and is only instantiated once per schema. The `ISubscriptionClientProxy`instances it creates should be unique per `IClientConnection` instance (i.e. transient). + +The server will listen for subscription registrations from your client proxy and send back published events when new data is available. It is up to your proxy to interprete these events, generate an appropriate result (including executing queries against the runtime), serialize the data and send it to the connected client on the other end. + +The details of implementing a custom graphql client proxy is beyond the scope of this documentation. Take a peek at the subscription library source code for some clues on how to get started. + +## Other Communication Options + +While websockets is the primary medium for persistant connections its not the only option. Internally, the library supplies an `IClientConnection` interface which encapsulates a raw connection websocket received from .NET. This interface is currently implemented as a `WebSocktClientConnection` which is responsible for reading and writing raw bytes to the socket. Its not a stretch of the imagination to implement your own custom client connection, invent a way to capture said connections and basically rewrite the entire communications layer of the subscriptions module. + +Please do a deep dive into the subscription code base to learn about all the intracasies of building your own communications layer and how you might go about registering it with the runtime. If you do try to tackle this very large effort don't hesitate to reach out. We're happy to partner with you and meet you half way on a solution if it makes sense for the rest of the community. + +## Performance Considerations + +At scale, its very possible that you may have lots of subscription events fired per second. Its important to understand how the receiving servers will process those events and plan accordingly. + +When the router receives an event it looks to see which receivers (a.k.a. connected clients) are subscribed to that event and queues up a work item for each one. Internally, this work is processed, concurrently if necessary, up to a server-configured maximum. Once this maximum is reached, new work will only begin as other work finishes up. + +Each work item is, for the most part, a standard query execution. But with lots of events being delivered on a server saturated with clients, each potentially having multiple subscriptions, along with regular queries and mutations executing as well...limits must be imposed otherwise CPU utilization could unreasonably spike...and it may spike regardless in some use cases. + +By default, the max number of work items the router will deliver simultaniously is `50`. This is a global, server-wide pool, shared amongst all registered schemas. You can manually adjust this value by changing it prior to calling `.AddGraphQL()`. This value defaults to a low number on purpose, use it as a starting point to dial up the max concurrency to a level you feel comfortable with in terms of performance and cost. The only limit here is server resources and other environment limitations outside the control of graphql. + +```csharp +// Startup.cs + +// Adjust the max concurrent communications value +// BEFORE calling .AddGraphQL() +SubscriptionServerSettings.MaxConcurrentReceiverCount = 50; + +services.AddGraphQL() + .AddSubscriptions(); +``` + +### Event Multiplication + +Think carefully about your production scenarios when you introduce subscriptions into your application. For each subscription event raised, each open subscription monitoring that event must execute a standard graphql query, with the supplied event data, to generate a result and send it to its connected client. + +If, for instance, you have `200 clients` connected to a single server, each with just `3 subscriptions` open against the one event, thats `600 individual queries` that must be executed to process the event completely. Suppose your server receives 5 mutations in rapid succession, all of which raise the event, thats a spike of `3,000 queries`, instantaneously, that the server must begin to process. + +Most of the work is queued, yes, but that's only to put a reasonable cap on the resources consumed. You'll still see a rapid and sustained increase in CPU utilization for as long as the queue is being processed. You might see a increase in wait time for new events to be delivered to awaiting clients if mutations keep occuring. If you are running in a cloud environment, think about how your CPU monitors might inadvertantly cause a "scale out" event because of a spike in load. + +Balancing the load can be difficult. Luckily there are some [throttling levers](/docs/reference/global-configuration#subscriptions) you can adjust. + +> Raising subscription events can exponentially increase the load on each of your servers. Think carefully when you deploy subscriptions to your application. diff --git a/docs/advanced/type-expressions.md b/docs/advanced/type-expressions.md index 2182a4a..ecf7999 100644 --- a/docs/advanced/type-expressions.md +++ b/docs/advanced/type-expressions.md @@ -12,7 +12,7 @@ These assumptions are made: - Fields that return reference types **can be** null - Fields that return value types **cannot be** null -- Fields that return Nullable value types (e.g. `int?`) **can be** be null +- Fields that return Nullable value types (e.g. `int?`) **can be** be null. - When a field returns an object that implements `IEnumerable` it will be presented to GraphQL as a "list of `TType`". Basically, if your method is able to return a value...then its valid as far as GraphQL is concerned. @@ -49,7 +49,7 @@ query {

-This action method could return a `Donut` or return `null`. But should the `donut` field allow a null value? The code certainly does and the rules above say fields that return a reference type can be null...but that's not what's important. Its ultimately your decision to decide if a null donut is allowed, not the C# compiler and not the assumptions made by the library. +This action method could return a `Donut` or returns `null`. But should the donut field, from a GraphQL perspective, allow a null return value? The code certainly does and the rules above say fields that return a reference type can be null...but that's not what's important. Its ultimately your decision to decide if a "null donut" is allowed, not the C# compiler and not the assumptions made by the library. On one hand, if a null value is returned, regardless of it being valid, the _outcome_ of the field is the same. When we return a null no child fields are processed. On the other hand, if null is not allowed we need to tell someone, let them know its nulled out not because it simply _is_ null but because a schema violation occurred. @@ -59,7 +59,7 @@ Most of the time, using the `TypeExpression` property of a field declaration att ```csharp -// Declare that an MUST be returned (null is invalid) +// Declare that a donut MUST be returned (null is invalid) // ---- // Schema Syntax: Donut! [Query("donut", TypeExpression = TypeExpressions.IsNotNull)] diff --git a/docs/assets/2022-10-graphql-aspnet-execution-diagrams.pdf b/docs/assets/2022-10-graphql-aspnet-execution-diagrams.pdf new file mode 100644 index 0000000..21ad594 Binary files /dev/null and b/docs/assets/2022-10-graphql-aspnet-execution-diagrams.pdf differ diff --git a/docs/assets/2022-10-graphql-aspnet-structural-diagrams.pdf b/docs/assets/2022-10-graphql-aspnet-structural-diagrams.pdf new file mode 100644 index 0000000..4e8375c Binary files /dev/null and b/docs/assets/2022-10-graphql-aspnet-structural-diagrams.pdf differ diff --git a/docs/assets/2022-10-subscription-server.pdf b/docs/assets/2022-10-subscription-server.pdf new file mode 100644 index 0000000..11f9256 Binary files /dev/null and b/docs/assets/2022-10-subscription-server.pdf differ diff --git a/docs/controllers/actions.md b/docs/controllers/actions.md index 6e4ea43..0ef63b7 100644 --- a/docs/controllers/actions.md +++ b/docs/controllers/actions.md @@ -562,12 +562,12 @@ public class BakeryController : GraphController ```javascript query { - searchDonuts(searchParams: - name: "jelly*" - filled: true - dayOld: false){ - id - name + searchDonuts( + name: "jelly*" + filled: true + dayOld: false){ + id + name } } ``` @@ -609,8 +609,7 @@ public class DonutSearchParams public class BakeryController : GraphController { [QueryRoot] - public IEnumerable - SearchDonuts(DonutSearchParams searchParams) + public IEnumerable SearchDonuts(DonutSearchParams searchParams) {/* ... */} } diff --git a/docs/controllers/authorization.md b/docs/controllers/authorization.md index 32de8ea..8512ea5 100644 --- a/docs/controllers/authorization.md +++ b/docs/controllers/authorization.md @@ -80,7 +80,7 @@ public class BakeryController : GraphController ## Use of IAuthorizationService -Under the hood, GraphQL taps into your `IServiceProvider` to obtain a reference to the `IAuthorizationService` that gets created when you configure `.AddAuthorization()` for policy enforcement rules. Take a look at the [Field Authorization](https://github.com/graphql-aspnet/graphql-aspnet/blob/master/src/graphql-aspnet/Middleware/FieldSecurity/Components/FieldAuthorizationMiddleware.cs) Middleware Component for the full picture. +Under the hood, GraphQL taps into your `IServiceProvider` to obtain a reference to the `IAuthorizationService` that gets created when you configure `.AddAuthorization()` for policy enforcement rules. Take a look at the [Schema Item Authorization Pipeline](https://github.com/graphql-aspnet/graphql-aspnet/tree/master/src/graphql-aspnet/Middleware/SchemaItemSecurity) for the full picture. ## When does Authorization Occur? @@ -90,21 +90,32 @@ _The Default "per field" Authorization workflow_ --- -In the diagram above we can see that user authorization in GraphQL ASP.NET makes use of the result from [ASP.NET's security pipeline](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction) but makes no attempt to interact with it. Whether you use Kerberos tokens, OAUTH2, username/password, API tokens or if you support 2-factor authentication or one-time-use passwords, GraphQL doesn't care. The entirety of your authentication and authorization pipeline is executed by GraphQL, no special arrangements or configuration is needed. +In the diagram above we can see that user authorization in GraphQL ASP.NET makes use of the result from [ASP.NET's security pipeline](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction). Whether you use Kerberos tokens, oauth2, username/password, API tokens or if you support 2-factor authentication or one-time-use passwords, GraphQL doesn't care. The entirety of your authentication and authorization scheme is executed by GraphQL, no special arrangements or configuration is needed. > GraphQL ASP.NET draws from your configured authentication/authorization solution. -Field requests are passed through a [pipeline](../reference/how-it-works#middleware-pipelines) where field authorization is enforced as a middleware component before the data is queried (i.e. before the resolver is invoked). Should a requestor not be authorized for a given field a `null` value is resolved and a message added to the response. +Execution directives and field resolutions are passed through a [pipeline](../reference/how-it-works#middleware-pipelines) where authorization is enforced as a series of middleware components before the respective handlers are invoked. Should a requestor not be authorized for a given schema item an action is taken: -Null propagation rules still apply to unauthorized fields meaning if the field cannot accept a null value its propagated up the field chain potentially nulling out a parent or parent of a parent depending on your schema. -By default, a single unauthorized result does not necessarily kill an entire query, it depends on the structure of your object graph. When a field request is terminated any down-stream child fields are discarded immediately but sibling fields or unrelated ancestors continue to execute as normal. +## Field Authorizations + +If a requestor is not authorized to a requested field a value of `null` is used as the resolved value and an error message is recorded to the query results. + +Null propagation rules still apply to unauthorized fields meaning if the field cannot accept a null value, its propagated up the field chain potentially nulling out a parent or "parent of a parent" depending on your schema. + +By default, a single unauthorized field result does not necessarily kill an entire query, it depends on the structure of your object graph and the query being executed. When a field request is terminated any down-stream child fields are discarded immediately but sibling fields or unrelated ancestors continue to execute as normal. Since this authorization occurs "per field" and not "per controller action" its possible to define the same security chain for POCO properties. This allows you to effectively deny access, by policy, to a single property of an instantiated object. Performing security checks for every field of data (especially in parent/child scenarios) has a performance cost though, especially for larger data sets. For most scenarios enforcing security at the controller level is sufficient. -## Authorization Failures are Obfuscated +### Field Authorization Failures are Obfuscated + +When GraphQL denies a requestor access to a field a message naming the field path is added to the response. This message is generic on purpose; `"Access denied to field '[query]/bakery/donuts'"`. To view more targeted reasons, such as specific policy failures, you'll need to expose exceptions on the request or turn on [logging](../logging/structured-logging). GraphQL automatically raises the `FieldSecurityChallengeCompleted` log event at a `Warning` level when a security check fails. -When GraphQL denies a requestor access to a field a message naming the field path is added to the response. This message is generic on purpose, `"Access denied to field '[query]/bakery/donuts'."`. To view more targeted reasons, such as policy failures, you'll need to expose exceptions on the request or turn on [logging](../logging/structured-logging). GraphQL automatically raises the `FieldSecurityChallengeCompleted` log event at a `Warning` level when a security check fails. +## Execution Directives Authorizations + +Execution directives are applied to the _query document_ before a query plan is created and it is the query plan that determines what field resolvers should be called. As a result, execution directives have the potential to alter the document structure and change how a query might be resolved. Because of this, not executing a query directive has the potential to change (or not change) the expected query to be different than what the requestor asked for. + +Therefore, if an execution directive fails authorization the query is rejected and not executed. The called will receive an error message as part of the response indicating the unauthorized directive. Like field authorization failures, the message is obfuscated and contains only a generic message. You'll need to expose exception on the request or turn on logging to see additional details. ## Authorization Methods @@ -126,3 +137,5 @@ public void ConfigureServices(IServiceCollection services) }); } ``` + +>Regardless of the authorization method chosen, **execution directives** are ALWAYS evaluated with a "per request" method. If a single execution directive fails, the whole query is dicarded. \ No newline at end of file diff --git a/docs/controllers/type-extensions.md b/docs/controllers/type-extensions.md index f547e79..d5a1a61 100644 --- a/docs/controllers/type-extensions.md +++ b/docs/controllers/type-extensions.md @@ -121,16 +121,16 @@ query { When you declare a type extension it will only be invoked in context of the type being extended. -When we return a field of data from a property, an instance of the object must to exist in order to retrieve the property value. The same is true for a `type extension` except that instead of calling a property getter on the instance we're handing the entire instance to our method and letting it figure out what it needs to do with the data to resolve the field. +When we return a value from a property, an instance of an object must exist in order to supply that value. That is to say if you want the `Name` property of a bakery, you need a bakery instance to retrieve it from. The same is true for a `type extension` except that instead of calling a property getter on the instance, graphql hands the entire object to our method and lets us figure out what needs to happen to resolve the field. -GraphQL senses the type being extended and finds a method parameter to match. It captures that parameter, hides it from the object graph and supplies it with the result of the parent field, in this case the resolution of field `bakery(id: 5)`. +GraphQL inspects the type being extended and finds a parameter on the method to match it. It captures that parameter, hides it from the object graph, and fills it with the result of the parent field, in this case the resolution of field `bakery(id: 5)`. This is immensely scalable: - There are no wasted cycles fetching `CakeOrders` unless the requestor specifically asks for them. - We have full access to [type expression validation](../advanced/type-expressions) and [model validation](./model-state) for our other method parameters. - Since its a controller action we have full access to graph action results and can return `this.Ok()`, `this.Error()` etc. to give a rich developer experience. -- [Field Authorization](./authorization) is also wired up for us. +- [Field Security](./authorization) is also wired up for us. - The bakery model is greatly simplified. #### Can every field be a type extension? @@ -138,8 +138,8 @@ This is immensely scalable: Theoretically, yes. But take a moment and think about performance. For basic objects with few dozen properties which is faster: - One database query to retrieve 24 columns of a single record then only use six in a graph result. -- Six separate database queries, one for each 10 character string value requested. +- Six separate database queries, one for each string value requested. -Type extensions shine in parent-child relationships when preloading data is a concern but be careful not to go isolating every graph field just to avoid retrieving data unless absolutely necessary. Retrieving a few extra bytes of string data is negligible compared to querying a database 20 times. Your REST APIs likely do it as well and they even transmit that data down the wire to the client and the client has to discard it. +Type extensions shine in parent-child relationships when preloading data is a concern but be careful not to go isolating every graph field just to avoid retrieving data. Fetching a few extra bytes from a database is negligible compared to querying a database 20 individual times. Your REST APIs likely do it as well and they even transmit that data down the wire to the client and the client has to discard it. It comes down to your use case. There are times when it makes sense to query data separately using type extensions and times when preloading whole objects is better. For many applications, once you've deployed to production, the queries being executed are finite. Design your model objects and extensions to be performant in the ways your data is being requested, not in the ways it _could be_ requested. diff --git a/docs/development/debugging.md b/docs/development/debugging.md index fba5786..73dc3f1 100644 --- a/docs/development/debugging.md +++ b/docs/development/debugging.md @@ -21,20 +21,3 @@ public void ConfigureServices(IServiceCollection services) }); } ``` - -## Increase the Query Timeout - -GraphQL will automatically abandon long running queries to prevent a resource drain. It may be helpful to up this timeout length in development. By default the timeout is `1 minute`. - -```csharp -// Startup.cs -public void ConfigureServices(IServiceCollection services) -{ - // Extending the default query timeout can help - // during extended debug sessions - services.AddGraphQL(options => - { - options.ExecutionOptions.QueryTimeout = TimeSpan.FromMinutes(30); - }); -} -``` diff --git a/docs/development/entity-framework.md b/docs/development/entity-framework.md index c01499d..615ad13 100644 --- a/docs/development/entity-framework.md +++ b/docs/development/entity-framework.md @@ -17,7 +17,7 @@ public void ConfigureServices(IServiceCollection services) }); } ``` -This default registration adds the `DbContext` to the DI container is as a `Scoped` service. Meaning one instance is generated per Http request. However, consider the following controller and query: +This default registration adds the `DbContext` to the DI container is as a `Scoped` service. Meaning one instance is generated per Http request. However, consider the following graph controller and query:
@@ -57,13 +57,13 @@ query {

-The `FoodController` contains two action methods both of which are executed by the query. While the controller itself is registered with the DI container as transient the `DbContext` is not, it is shared between the controller instances. This can result in an exception being thrown : +The `FoodController` contains two action methods both of which are executed by the query. This means two instances of the controller are needed, once for each field resolution, since they are executed in parallel. While the controller itself is registered with the DI container as transient the `DbContext` is not, it is shared between the controller instances. This can result in an exception being thrown : ![Ef Core Error](../assets/ef-core-error.png) This is caused by graphql attempting to execute both controller actions simultaneously. Ef Core will reject multiple active queries. There are a few ways to handle this and each comes with its own trade offs: -## Register DbContext as transient +## Register DbContext as Transient One way to correct this problem is to register your DbContext as a transient object. @@ -99,6 +99,6 @@ public void ConfigureServices(IServiceCollection services) ``` This will instruct graphql to execute each encountered controller action one after the other. Your scoped `DbContext` would then be able to process the queries without issue. -The tradeoff with this method is a minor decrease in processing time since the queries are called in sequence. All other field resolutions would be executed in parallel. +The tradeoff with this method is a decrease in processing time since the queries are called in sequence. All other field resolutions would be executed in parallel. -If your application has other resources or services that are not thread safe it can be beneficial to isolate the other resolver types as well. You can add them to the ResolverIsolation configuration option as needed. +If your application has other resources or services that may have similar restrictions, it can be beneficial to isolate the other resolver types as well. You can add them to the ResolverIsolation configuration option as needed. diff --git a/docs/introduction/made-for-aspnet-developers.md b/docs/introduction/made-for-aspnet-developers.md index e420c98..cbe955f 100644 --- a/docs/introduction/made-for-aspnet-developers.md +++ b/docs/introduction/made-for-aspnet-developers.md @@ -75,7 +75,7 @@ Also, if you are integrating into an existing project, you'll find a lot of util ## Scoped Dependency Injection -Services are injected into graph controllers in the same manner as MVC controllers and with the same scope resolution as the HTTP request. Yes, your HTTP request level Entity Framework `DbContext` will be carried forward through all field resolutions. +Services are injected into graph controllers in the same manner as MVC controllers and with the same scope resolution as the HTTP request. ## User Authorization diff --git a/docs/logging/standard-events.md b/docs/logging/standard-events.md index 08c3944..5447a59 100644 --- a/docs/logging/standard-events.md +++ b/docs/logging/standard-events.md @@ -1,10 +1,10 @@ --- id: standard-events -title: Standard Events +title: Standard Logging Events sidebar_label: Standard Events --- -GraphQL ASP.NET tracks 19 standard events. Most of these are recorded during the execution of a query. Some, such as those around field resolution, can be recorded many times in the course of a single request. +GraphQL ASP.NET tracks many standard events. Most of these are recorded during the execution of a query. Some, such as those around field resolution, can be recorded many times in the course of a single request. _**Common Event Properties**_ @@ -20,11 +20,11 @@ _Constants for event names and ids can be found at_ `GraphQL.AspNet.Logging.LogE _Constants for all log entry properties can be found at_ `GraphQL.AspNet.Logging.LogPropertyNames` -## Startup Events +## Schema Level Events ### Schema Route Registered -This event is recorded when GraphQL successfully registers an entry in the ASP.NET MVC's route table to accept requests for a target schema. This event is raised once per application instance. +This event is recorded when GraphQL successfully registers an entry in the ASP.NET MVC's route table to accept requests for a target schema. This event is recorded once per application instance. **Important Properties** @@ -131,52 +131,141 @@ This is event is recorded when the final result for the request is generated and | _OperationRequestId_ | A unique id identifying the overall request. | | _HasData_ | `true` or `false` indicating if at least one data value was included in the result | | _HasErrors_ | `true` or `false` indicating if at least one error message was included in the result | +| _TotalExecutionMs_ | A numerical value indicating the total runtime of the request, in milliseconds. | -## Field Level Events +### Request Cancelled -After a query plan has been created GraphQL ASP.NET begins resolving each field needed to fulfill the request. This group of events is raised for each item of each field that is processed. Since all fields are executed asynchronously (even if the resolvers themselves are synchronous) the order in which the events are recorded can be unpredictable and overlap between fields can occur. Using the recorded date along with the `PipelineRequestId` can help to filter the noise. +This is event is recorded when the a request is explicitly cancelled, usually by the underlying HTTP connection. + +**Important Properties** + +| Property | Description | +| -------------------- | ------------------------------------------------------------------------------------- | +| _OperationRequestId_ | A unique id identifying the overall request. | +| _TotalExecutionMs_ | A numerical value indicating the total runtime of the request, in milliseconds. | -### Field Resolution Started -This event is raised when a new field is queued for resolution. +### Request Timeout + +This is event is recorded when the a request is is cancelled due to reaching a maximum timeout limit defined by the schema. **Important Properties** -| Property | Description | -| -------------------- | --------------------------------------------------------------------------------------- | -| _PipelineRequestId_ | A unique id identifying the individual field request. | -| _FieldExecutionMode_ | Indicates if this pipeline is being executed for a `single source item` or as a `batch` | -| _FieldPath_ | The path of the field being resolved, e.g. `[type]/Donut/id` | +| Property | Description | +| -------------------- | ------------------------------------------------------------------------------------- | +| _OperationRequestId_ | A unique id identifying the overall request. | +| _TotalExecutionMs_ | A numerical value indicating the total runtime of the request, in milliseconds. | + + +## Directive Level Events + +### Execution Directive Applied + +This event is recorded when an execution directive is successfully executed against an `IDocumentPart` on an incoming query. + +This is event is recorded when the final result for the request is generated and is returned from the runtime to be serialized. No actual data values are recorded to the logs to prevent leaks of potentially sensitive information. + +**Important Properties** + +| Property | Description | +| -------------------- | ------------------------------------------------------------------------------------- | +| _SchemaTypeName_ | The full .NET type name of the schema type this plan targets | +| _DirectiveName_ | The name of the directive as it exists in the target schema | +| _DirectiveInternalName_ | The .NET class name of the directive | +| _DirectiveLocation_ | The target location in the query document (e.g. FIELD, FRAGMENT_SPREAD etc.) | + +### Type System Directive Applied + +This event is recorded when a schema is first generated and all known type system directives are applied to the schema items +to which they are attached. An entry is recorded for each directive applied. + +**Important Properties** + +| Property | Description | +| -------------------- | ------------------------------------------------------------------------------------- | +| _SchemaTypeName_ | The full .NET type name of the schema type this plan targets | +| _SchemaItemPath_ | The path of the item being resolved, e.g. `[type]/Donut/id` | +| _DirectiveName_ | The name of the directive as it exists in the target schema | +| _DirectiveInternalName_ | The .NET class name of the directive | +| _DirectiveLocation_ | The target location in the query document (e.g. FIELD, FRAGMENT_SPREAD etc.) | -### Field Authorization Started -This is event is raised when a field is sent for authorization prior to being resolved. +## Auth Events + +### Item Authentication Started + +This is event is record when a security context on a query is authenticated to determine an +appropriate ClaimsPrincipal to use for authorization. + +**Important Properties** + +| Property | Description | +| ------------------- | ------------------------------------------------------------ | +| _PipelineRequestId_ | A unique id identifying the individual field request. | +| _SchemaItemPath_ | The path of the item being resolved, e.g. `[type]/Donut/id` | + + +### Item Authentication Completed + +This is event is recorded after a security context is authenticated and a ClaimsPrincipal was generated (if required). + +**Important Properties** + +| Property | Description | +| ----------------------------- | ------------------------------------------------------------ | +| _PipelineRequestId_ | A unique id identifying the individual field request. | +| _SchemaItemPath_ | The path of the item being resolved, e.g. `[type]/Donut/id` | +| _Username_ | The value of the `Name` field on the active Identity or null| +| _AuthenticationScheme_ | The key representing the chosen authentication schema (e.g. `Bearer`, `Kerberos` etc.) | +| _AuthenticationSchemaSuccess_ | `true` if authentication against the scheme was successful | +| _SchemaItemPath_ | The path of the item being resolved, e.g. `[type]/Donut/id` | + +### Item Authorization Started + +This is event is recorded when an authenticated user is authorized against schema item (typically a Field or Directive). **Important Properties** | Property | Description | | ------------------- | ------------------------------------------------------------ | | _PipelineRequestId_ | A unique id identifying the individual field request. | -| _FieldPath_ | The path of the field being resolved, e.g. `[type]/Donut/id` | -| _Username_ | the value of `this.User.Identity.Name` or null | +| _SchemaItemPath_ | The path of the item being resolved, e.g. `[type]/Donut/id` | +| _Username_ | The value of the `Name` field on the active Identity or null| -### Field Authorization Completed +### Item Authorization Completed -This is event is raised after a field authorization has completed. +This is event is recorded after a schema item authorization has completed. **Important Properties** | Property | Description | | --------------------- | -------------------------------------------------------------------------- | | _PipelineRequestId_ | A unique id identifying the individual field request. | -| _FieldPath_ | The path of the field being resolved, e.g. `[type]/Donut/id` | -| _Username_ | the value of `this.User.Identity.Name` or null | +| _SchemaItemPath_ | The path of the item being resolved, e.g. `[type]/Donut/id` | +| _Username_ | The value of the `Name` field on the active Identity or null | | _AuthorizationStatus_ | `Skipped`, `Authorized` or `Unauthorized` | | _LogMessage_ | An internal message containing an explanation of why authorization failed. | + +## Field Level Events + +After a query plan has been created GraphQL ASP.NET begins resolving each field needed to fulfill the request. This group of events is recorded for each item of each field that is processed. Since all fields are executed asynchronously (even if the resolvers themselves are synchronous) the order in which the events are recorded can be unpredictable and overlap between fields can occur. Using the recorded date along with the `PipelineRequestId` can help to filter the noise. + +### Field Resolution Started + +This event is recorded when a new field is queued for resolution. + +**Important Properties** + +| Property | Description | +| -------------------- | --------------------------------------------------------------------------------------- | +| _PipelineRequestId_ | A unique id identifying the individual field request. | +| _FieldExecutionMode_ | Indicates if this pipeline is being executed for a `single source item` or as a `batch` | +| _FieldPath_ | The path of the field being resolved, e.g. `[type]/Donut/id` | + ### Field Resolution Completed -This is event is raised when a field completes its execution pipeline and a result is generated. No actual data values are recorded to the logs to prevent leaks of potentially sensitive information. +This is event is recorded when a field completes its execution pipeline and a result is generated. No actual data values are recorded to the logs to prevent leaks of potentially sensitive information. **Important Properties** @@ -188,11 +277,11 @@ This is event is raised when a field completes its execution pipeline and a resu ## Controller Level Events -After the security challenge has completed, but before field resolution is completed, if the pipeline executes a controller method to resolve the field these events will be raised. If the target resolver of the field is a property or POCO method, these events are skipped. +After the security challenge has completed, but before field resolution is completed, if the pipeline executes a controller method to resolve the field these events will be recorded. If the target resolver of the field is a property or POCO method, these events are skipped. ### Action Invocation Started -This event is raised when a controller begins processing a request to execute an action method. +This event is recorded when a controller begins processing a request to execute an action method. **Important Properties** @@ -222,7 +311,7 @@ This event occurs after the controller has processed the input objects and valid ### Action Invocation Completed -This event is raised when a controller completes the invocation of an action method and a result is created. +This event is recorded when a controller completes the invocation of an action method and a result is created. **Important Properties** @@ -236,7 +325,7 @@ This event is raised when a controller completes the invocation of an action met ### Action Invocation Exception -This event is raised by the controller if it is unable to invoke the target action method. This usually indicates some sort of data corruption or failed conversion of source data to the requested parameter types of the target action method. This can happen if the query plan or variables collection is altered by a 3rd party outside of the normal pipeline. Should this event occur the field will be abandoned and a null value returned as the field result. Child fields to this instance will not be processed but the operation will continue to attempt to resolve other sibling fields and their children. +This event is recorded by the controller if it is unable to invoke the target action method. This usually indicates some sort of data corruption or failed conversion of source data to the requested parameter types of the target action method. This can happen if the query plan or variables collection is altered by a 3rd party outside of the normal pipeline. Should this event occur the field will be abandoned and a null value returned as the field result. Child fields to this instance will not be processed but the operation will continue to attempt to resolve other sibling fields and their children. **Important Properties** @@ -251,7 +340,7 @@ This event is raised by the controller if it is unable to invoke the target acti ### Action Unhandled Exception -This event is raised if an unhandled exception occurs within the controller action method body. Should this event occur the field will be abandoned and a null value returned as the result of the field. Child fields will not be processed but the operation will continue to attempt to resolve other sibling fields and their children. +This event is recorded if an unhandled exception occurs within the controller action method body. Should this event occur the field will be abandoned and a null value returned as the result of the field. Child fields will not be processed but the operation will continue to attempt to resolve other sibling fields and their children. **Important Properties** @@ -268,7 +357,7 @@ This event is raised if an unhandled exception occurs within the controller acti ### Unhandled Exception -This event is raised if any pipeline invocation is unable to recover from an error. If this is event is raised the request is abandoned and an error status is returned to the requestor. This event is always raised at a `Critical` log level. This event will be immediately followed by a `Request Completed` event. +This event is recorded if any pipeline invocation is unable to recover from an error. If this is event is recorded the request is abandoned and an error status is returned to the requestor. This event is always recorded at a `Critical` log level. This event will be immediately followed by a `Request Completed` event. **Important Properties** diff --git a/docs/logging/structured-logging.md b/docs/logging/structured-logging.md index e9d0c22..af46ba0 100644 --- a/docs/logging/structured-logging.md +++ b/docs/logging/structured-logging.md @@ -61,7 +61,7 @@ public class BakeryController : GraphController } ``` -`GraphLogEntry` is an untyped implementation of `IGraphLogEvent` and can be used on the fly for quick operations. +> `GraphLogEntry` is an untyped implementation of `IGraphLogEvent` and can be used on the fly for quick operations. ## Custom ILoggers diff --git a/docs/logging/subscription-events.md b/docs/logging/subscription-events.md new file mode 100644 index 0000000..a8fa408 --- /dev/null +++ b/docs/logging/subscription-events.md @@ -0,0 +1,172 @@ +--- +id: subscription-events +title: Subscription Logging Events +sidebar_label: Subscription Events +--- + +GraphQL ASP.NET tracks some special events related to the management of subscriptions. They are outlined below. + +_**Common Event Properties**_ + +The [common event properties](./standard-events.md) outlined on the standard events page apply to all subscription events. + +## Schema Level Events + +### Subscription Route Registered + +This event is recorded when GraphQL successfully registers an entry in the ASP.NET MVC's route table to accept requests for a target schema as well as +register the middleware component necessary to receive websocket requests. + +**Important Properties** + +| Property | Description | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| _SchemaTypeName_ | The full name of your your schema type. For most single schema applications this will be `GraphQL.AspNet.Schemas.GraphSchema`. | +| _SchemaSubscriptionRoutePath_ | The relative URL that was registered for the schema type, e.g. `'/graphql` | + + +## Client Connection Events + + ### Client Registered + +This event is recorded when GraphQL successfully accepts a client and has assigned a client proxy to manage the connection. This event is recorded just prior to the connection is "started" and messaging begins. + +**Important Properties** + +| Property | Description | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| _ClientId_ | A unique id asigned to the client when it first connected. | +| _SchemaTypeName_ | The full name of your your schema type. For most single schema applications this will be `GraphQL.AspNet.Schemas.GraphSchema`. | +| _ClientTypeName_ | The full name of the assigned client proxy type. In general, a different type is used per messaging protocol. | +| _ClientProtocol_ | The protocol negotiated by the client and server that will be used for the duration of the connection. | + + ### Client Dropped + +This event is recorded when GraphQL is releasing a client. The connection has been "stopped" and no additional messagings are being broadcast. This event occurs just before the HTTP connection is closed. + +**Important Properties** + +| Property | Description | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| _ClientId_ | A unique id asigned to the client when it first connected. | +| _ClientTypeName_ | The full name of the assigned client proxy type. In general, a different type is used per messaging protocol. | +| _ClientProtocol_ | The protocol negotiated by the client and server that will be used for the duration of the connection. | + + + ### Unsupported Client Protocol + +This event is recorded when GraphQL attempts to create an appropriate proxy class for the connection but no such proxy could be deteremined from the details providied in the initial request. In general, this means the provided websocket sub protocols did not match a supported protocol for this server and schema combination. + +| Property | Description | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| _SchemaTypeName_ | The full name of your your schema type. For most single schema applications this will be `GraphQL.AspNet.Schemas.GraphSchema`. | +| _ClientProtocol_ | The protocol(s) requested by the client connection that were not accepted | + +**Important Properties** + +| Property | Description | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| _ClientId_ | A unique id asigned to the client when it first connected. | +| _ClientTypeName_ | The full name of the assigned client proxy type. In general, a different type is used per messaging protocol. | + + +## Client Messaging Events + +### Subscription Event Received + +This event is recorded by a client proxy when it received an event from the router and has determined that it should be handled. + +**Important Properties** + +| Property | Description | +| ---------------- | --------------------------------------------------------------------- | +| _ClientId_ | A unique id asigned to the client when it first connected. | +| _SchemaTypeName_ | The full name of your your schema type. For most single schema applications this will be `GraphQL.AspNet.Schemas.GraphSchema`. | | +| _SubscriptionPath_ | The path to the target top-level subscription field in the schema | +| _SubscriptionCount_ | The number of registered subscriptions, for this client, that will receive this event. | +| _SubscriptionIds_ | A comma seperated list of id values representing the subscriptions that will receive this event. | +| _MachineName_ | The `Environment.MachineName` of the current server. | + +### Subscription Registered + +This event is recorded by a client proxy when it starts a new subscription on behalf of its connected client. + +**Important Properties** + +| Property | Description | +| ---------------- | --------------------------------------------------------------------- | +| _ClientId_ | A unique id asigned to the client when it first connected. | +| _SubscriptionPath_ | The path to the target top-level subscription field in the schema | +| _SubscriptionId_ | The subscription id requested by the client. | + + +### Subscription Registered + +This event is recorded by a client proxy when it unregistered and abandons an existing subscription. This may be due to the server ending the subscription or the client requesting it be stopped. + +**Important Properties** + +| Property | Description | +| ---------------- | --------------------------------------------------------------------- | +| _ClientId_ | A unique id asigned to the client when it first connected. | +| _SubscriptionPath_ | The path to the target top-level subscription field in the schema | +| _SubscriptionId_ | The subscription id requested by the client. | + + +### Client Message Received + +This event is recorded by a client proxy when it successfully receives and deserializes a message from its connected client. Not all client proxies may record this event. The messages a client proxy defines must implement `ILoggableClientProxyMessage` in order to use this event. + +**Important Properties** + +| Property | Description | +| ---------------- | --------------------------------------------------------------------- | +| _ClientId_ | A unique id asigned to the client when it first connected. | +| _MessageType_ | A string value representing the type of the message that was received | +| _MessageId_ | The globally unique message id that was assigned to the incoming message. | + +### Client Message Sent + +This event is recorded by a client proxy when it successfully serializes and transmits a message to its connected client. Not all client proxies may record this event. The messages a client proxy defines must implement `ILoggableClientProxyMessage` in order to use this event. + +**Important Properties** + +| Property | Description | +| ---------------- | --------------------------------------------------------------------- | +| _ClientId_ | A unique id asigned to the client when it first connected. | +| _MessageType_ | A string value representing the type of the message that was received | +| _MessageId_ | The globally unique message id that was assigned to the incoming message. | + +## Subscription Events + +Subscription events refer to the events that are raised from mutations and processed by client proxies with registered subscriptions against those events. + + ### Subscription Event Published + + This event is recorded just after an event is handed off to a `ISubscriptionEventPublisher` for publishing to a storage medium. Custom publishers do not need to record this event manually. + + +**Important Properties** + +| Property | Description | +| ---------------- | ---------------------------------------------------------------------| +| _SchemaType_ | The schema type name as it was published. This will likely include additional information not recorded in standard schema level events. | +| _DataType_ | The data type name of the data object that was published. | +| _SubscriptionEventId_ | The globally unique id of the subscription event. | +| _SubscriptionEventName_ | The name of the event as its defined in the schema. | +| _MachineName_ | The `Environment.MachineName` of the current server. | + + ### Subscription Event Received + + This event is recorded by the event router just after it receives an event. The router will then proceed to forward the event to the correct client instances for processing. + +**Important Properties** + +| Property | Description | +| ---------------- | ---------------------------------------------------------------------| +| _SchemaType_ | The schema type name as it was recevied. This will likely include additional information not recorded in standard schema level events. | +| _DataType_ | The data type name of the data object that was received. | +| _SubscriptionEventId_ | The globally unique id of the subscription event. | +| _SubscriptionEventName_ | The name of the event as its defined in the schema. | +| _MachineName_ | The `Environment.MachineName` of the current server. | + diff --git a/docs/reference/global-configuration.md b/docs/reference/global-configuration.md new file mode 100644 index 0000000..57db389 --- /dev/null +++ b/docs/reference/global-configuration.md @@ -0,0 +1,71 @@ +--- +id: global-configuration +title: Global Configuration +sidebar_label: Global Configuration +--- + +Global configuration settings affect the entire server instance, they are not restricted to a single schema registration. All global settings are optional and define resonable default values. Use these to fine tune your server environment. You should change any global settings BEFORE calling `.AddGraphQL()`. + +```csharp +// Startup.cs +public void ConfigureServices(IServiceCollection services) +{ + // ************************* + // CONFIGURE GLOBAL SETTINGS HERE + // ************************* + + services.AddGraphQL(options => + { + // ************************* + // CONFIGURE YOUR SCHEMA HERE + // ************************* + }); +} +``` + +## General +### ControllerServiceLifetime + +The configured service lifetime that all discovered controllers and directives will be registered as within the DI container during any schema's setup +process. + +```csharp +GraphQLProviders.GlobalConfiguration.ControllerServiceLifetime = ServiceLifetime.Transient; +``` +| Default Value | Acceptable Values | +| ------------- | ----------------- | +| `Transient ` | `Transient`, `Scoped`, `Singleton` | + +
+WARNING: Registering graph controllers as anything other than transient can cause unexpected behavior and result in unexplained crashes, data loss, data exposure and security holes. Consider restructuring your application before changing this setting. Adjusting this value should be a last resort, not a first option. +
+ +## Subscriptions + +### MaxConcurrentReceiverCount + +Indicates the maximum number of entities (i.e. client connections) that will receive a raised subscription event on this server instance. If there are more receivers than this configured limit the others are queued and will recieve the event in turn once as others finish processing it. + +```csharp +SubscriptionServerSettings.MaxConcurrentReceiverCount = 50; +``` + +| Default Value | Acceptable Values | +| ------------- | ----------------- | +| `50` | > 0 | + + +### MaxConnectedClientCount + +Indicates the maximum number of client connections this server instance will accept, combined, across all schemas. If this limit is reached a new connection will be automatically rejected even if ASP.NET was willing to accept it. + + +```csharp +SubscriptionServerSettings.MaxConnectedClientCount = null; +``` + +| Default Value | Acceptable Values | +| ------------- | ----------------- | +| `null` | null OR > 0 | + +_Note:_ `null` _indicates that no limits will be enforced._ \ No newline at end of file diff --git a/docs/reference/how-it-works.md b/docs/reference/how-it-works.md index 709ef16..4119495 100644 --- a/docs/reference/how-it-works.md +++ b/docs/reference/how-it-works.md @@ -12,29 +12,31 @@ sidebar_label: How it Works #### Object Templating -When your application starts the runtime begins by inspecting the registered `ISchema` types in your `Startup.cs` for the different options you've declared and sets off gathering a collection of the possible graph types that may be required. +When your application starts the runtime begins by inspecting the registered schemas declared in your `Startup.cs` for the different options you've declared and sets off gathering a collection of the possible graph types that may be required. -For each type, it generates a template that describes _how_ you've asked GraphQL to use your classes. By inspecting declared attributes and the `System.Type` metadata it generates the appropriate information to create everything GraphQL ASP.NET will need to fulfill a query. Information such as input and output parameters for methods, property types, custom type naming, implemented interfaces, union declarations, field path definitions, validation requirements and enforced authorization policies are all gathered and stored at the application level under the configured `IGraphTypeTemplateProvider`. +For each type it discovers, it generates a template that describes _how_ you've asked GraphQL to use your classes. By inspecting declared attributes and the `System.Type` metadata it generates the appropriate information to create everything GraphQL ASP.NET will need to fulfill a query. Information such as input and output parameters for methods, property types, custom type naming, implemented interfaces, union declarations, field path definitions, validation requirements and enforced authorization policies are all gathered and stored at the application level under the globally configured `IGraphTypeTemplateProvider`. -From this collection of metadata, GraphQL then generates the appropriate `IGraphType` objects for each of your schemas based on their individual configurations. By default, this completed `ISchema` is stored as a singleton in your DI container. There shouldn't be a need to change this but if your use case demands it you can perform a custom schema registration and generate a new schema instance per request/operation. This is not a trivial task though, there is a lot to do when generating a schema should you take over the process. +From this collection of metadata, GraphQL then generates the appropriate `IGraphType` objects for each of your schemas based on their individual configurations. By default, this completed a `ISchema` is stored as a singleton in your DI container. **How does it know what objects to include?** -GraphQL ASP.NET has a few methods of determining what objects to include in your schema. By default, it will inspect your application (the entry assembly) for any public classes that inherit from `GraphController` and work from there. It checks every tagged query and mutation method, looks at every return value and every method parameter to find relevant scalars, enums and object types then inspects each one in turn, deeper and deeper down your object chain, to create a full map. It will even inspect the arbitrary interfaces implemented on each of your consumed objects. If that interface is ever used as a return type on an action method or a property, its automatically promoted to a graph type and included in the schema. +GraphQL ASP.NET has a few methods of determining what objects to include in your schema. By default, it will inspect your application (the entry assembly) for any public classes that inherit from `GraphController` or `GraphDirective` and work from there. It checks every tagged query and mutation method, looks at every return value and every method parameter to find relevant scalars, enums and object types then inspects each one in turn, deeper and deeper down your object chain, to create a full map. It will even inspect the arbitrary C# interfaces implemented on each of your consumed objects. If that interface is ever used as a return type on an action method or a property, its automatically promoted to a graph type and included in the schema. You have complete control of what to include. Be that including additional assemblies, preventing the inclusion of the startup assembly, manually specifying each model class and controller etc. Attributes exist such as `[GraphSkip]` to exclude certain properties, methods or entire classes and limit the scope of the inclusion. On the other side of the fence, you can configure it to only accept classes with an explicitly declared `[GraphType]` attribute, ignoring everything else. And for the most control, disable everything and manually call `.AddType()` at startup for each class you want to have in your schema (controllers included). GraphQL will then happily generate declaration errors when it can't find a reference declared in your controllers. This can be an effective technique in spotting data leaks or rogue methods that slipped through a code review. Configure a unit test to generate a schema with different inclusion rules per environment and you now have an automatic CI/CD check in place to give your developers more freedom to write code during a sprint and only have to worry about configurations when submitting a PR. -You can even go so far as to add a class to the schema but prevent its publication in introspection queries which can provide some helpful obfuscation. Alternatively, just disable introspection queries altogether. While this does cause client tooling to complain endlessly and makes front-end development much harder; if you and your consumers (like your UI) can agree ahead of time on the query syntax then there is no issue. +You can even go so far as to add a class to the schema but prevent its publication in introspection queries which can provide some helpful obfuscation. Alternatively, just disable introspection queries altogether. While this does cause client tooling to complain endlessly and makes front-end development much harder; if you and your consumers (like your UI) can agree ahead of time on the query syntax then there is no issue. #### Middleware Pipelines -Similar to how ASP.NET utilizes a middleware pipeline to fulfill an HTTP request, GraphQL ASP.NET follows suit to fulfill a graphQL request. Major tasks like validation, parsing, field resolution and result packaging are just [middleware components](../advanced/middleware) added to a chain of tasks and executed to complete the operation. +Similar to how ASP.NET utilizes a middleware pipeline to fulfill an HTTP request, GraphQL ASP.NET follows suit to fulfill a graphQL request. Major tasks like validation, parsing, field resolution and result packaging are just [middleware components](../reference/middleware) added to a chain of tasks and executed to complete the operation. -At the same time as its constructing your schema, GraphQL sets up the 4 primary pipelines and store them in the DI container as an `ISchemaPipeline`. Each pipeline can be extended, reworked or completely replaced as needed for your use case. +At the same time as its constructing your schema, GraphQL sets up the 4 primary pipelines and stores them in the DI container as an `ISchemaPipeline`. Each pipeline can be extended, reworked or completely replaced as needed for your use case. ## Query Execution -Query execution is performed in a phased approach. For the sake of brevity we've left out the HTTP request steps required to invoke the GraphQL runtime but you can inspect the [DefaultGraphQLHttpProcessor](https://github.com/graphql-aspnet/graphql-aspnet/blob/master/src/graphql-aspnet/Defaults/DefaultGraphQLHttpProcessor%7BTSchema%7D.cs) and read through the code. +Query execution can be broken down into a series of phaes. For the sake of brevity we've left out the HTTP request steps required to invoke the GraphQL runtime but you can inspect the [DefaultGraphQLHttpProcessor](https://github.com/graphql-aspnet/graphql-aspnet/blob/master/src/graphql-aspnet/Defaults/DefaultGraphQLHttpProcessor%7BTSchema%7D.cs) and read through the code. + +Note: The concept of a phase here is just for organizing the information, there is no concrete "phase" value managed by the pipelines. ### Phase 1: Parsing & Validation @@ -100,27 +102,24 @@ query { _Sample query used as a reference example in this section_ -The supplied query document (top right in the example) is ran through a compilation cycle to generate an `IGraphQueryPlan`. It is first lexed into a series of tokens representing the various parts; things like curly braces, colons, strings etc. Then it parses those tokens into a collection of `SyntaxNodes` (creating an Abstract Syntax Tree) representing concepts like `FieldNode`, `InputValueNode`, and `OperationTypeNode` following the [graphql specification rules for source text documents](https://spec.graphql.org/October2021/#sec-Source-Text). +The supplied query document (top right in the example) is ran through a compilation cycle to ultimately generate an `IGraphQueryPlan`. It is first lexed into a series of tokens representing the various parts; things like curly braces, colons, strings etc. Then it parses those tokens into a collection of `SyntaxNodes` (creating an Abstract Syntax Tree) representing concepts like `FieldNode`, `InputValueNode`, and `OperationTypeNode` following the [graphql specification rules for source text documents](https://spec.graphql.org/October2021/#sec-Source-Text). -Once parsed, the runtime will execute its internal rules engine against the generated `ISyntaxTree`, using the targeted `ISchema`, to create a query plan where it marries the AST with concrete structures such as controllers, action methods and POCOs. It is at this stage where the `hero` field in the example is matched to the `HeroController` with its appropriate `IGraphFieldResolver` to invoke the `RetrieveHero` action method. +Once parsed, the runtime will execute its internal rules engine against the generated `ISyntaxTree`, using the targeted `ISchema`, to create a query plan where it marries the nodes AST with concrete structures such as controllers, action methods and POCOs. It is at this stage where the `hero` field in the example is matched to the `HeroController` with its appropriate `IGraphFieldResolver` to invoke the `RetrieveHero` action method. While generating a query plan the rules engine will do its best to complete an analysis of the entire document and return to the requestor every error it finds. Depending on the errors though, it may or may not be able to catch them all. For instance, a syntax error, like a missing `}`, will preclude generating a query plan so errors centered around invalid field names or a type expression mismatch won't be caught until the syntax error is fixed (just like any other compiler). - ### Phase 2: Execution The engine now has a completed query plan that describes: -- The named operations declared in the document (or the single anonymous operation in the example above) -- The top level fields and every child field for each operation +- The named operation in the document to be executed (or the single anonymous operation in the example above) +- The top level fields and every child field on the chosen operation. **Its successfully validated that:** - All the referenced fields for the graph types exist and are valid where requested - Required input arguments have been supplied and their data is "resolvable" - - This just means that we've validated that a number is a number, arguments on input objects exist etc. - -The specifics provided by the client are now brought into play and the runtime will select the correct operation, validate any provided variable values against the operation's declarations and select the first set of fields to execute. + - This just means that we've validated that a number is a number, named fields on input objects exist etc. **For each field, the runtime will:** @@ -128,78 +127,15 @@ The specifics provided by the client are now brought into play and the runtime w - Generate a field execution context containing the necessary data about the source data, arguments and resolver. - Authenticate the user to the field. - Execute the resolver to fetch a data value. -- Invoke any child field requested. +- Invoke any child fields requested. #### Resolving a Field -GraphQL use the phrase "resolver" to describe a method that, for a given field, takes in parameters and generates a result. +GraphQL uses the phrase "resolver" to describe a method that, for a given field, takes in parameters and generates a result. At startup, GraphQL ASP.NET automatically creates resolver references for your controller methods, POCO properties and tagged POCO methods. These are nothing more than delegates for method invocations; for properties it uses the getter registered with `PropertyInfo`. -> **_Performance Note_**: The library makes heavy use of Lambdas generated from compiled Expression Trees to call its resolvers and for instantiating input objects when generating a field request. As a result, its many orders of magnitude faster than baseline reflected calls and nearly as performant as precompiled code. - -**Concerning the N+1 Problem** - -If you're reading this document you're probably aware of the [N+1 problem](https://itnext.io/what-is-the-n-1-problem-in-graphql-dd4921cb3c1a) and the concept of Data Loaders as they relate to other GraphQL implementations. The section in this documentation on [batch operations](../controllers/batch-operations) covers it in detail, but suffice it to say this library can handle 1:1, 1:N and N:N relationships with no trouble. These are implemented as type extensions through controller methods. - -Lets use a scenario where we query a set of managers and for each manager, their employees; setting up a 1:N concern. Using a `[BatchTypeExtension]` we can query all the needed children at once then split them out to the individual managers. - -```js -// Assume that in this query the fields for -// "managers" and "employees" both return an array of people -query { - managers(lastNameLike: "Sm*") { - id - name - employees { - id - name - title - } - } -} -``` - -Your controller might look like this: - -```csharp -// C# Controller -public class ManagersController : GraphController -{ - // constructor with service injection omitted - - [QueryRoot("managers")] - public async Task> RetrieveManagers(string lastNameLike) - { - return await _service.SearchManagers(lastNameLike); - } - - // Declare a new field that: - // *Extends the Manager type - // *Is called "employees" - // *Returns a collection of employees per manager: IEnumerable - [BatchTypeExtension(typeof(Manager), "employees", typeof(IEnumerable))] - public async Task RetrieveEmployees(IEnumerable managers) - { - // the managers parameter here represents the entire collection of managers - // previously retrieved on this request, it is not part of the "employees" - // field definition. The runtime works this out automatically - // and the parameter is not exposed on the object graph. - IEnumerable employees = await _service - .RetrieveEmployeesByManager(managers.Select(mgr => mgr.Id)); - - // We have to tell GraphQL how to map the data for each manager. - // - // StartBatch() is a convience method that builds - // an IDictionary> - return this.StartBatch() - .FromSource(managers, mgr => mgr.Id) - .WithResults(employees, emp => emp.ManagerId) - .Complete(); - } -} -``` - +> **_Performance Note_**: The library makes heavy use of compiled Expression Trees to call its resolvers and for instantiating input objects. As a result, its many orders of magnitude faster than baseline reflected calls and nearly as performant as precompiled code. **Concerning Proxy Libraries (e.g. EF Core Proxies)** @@ -227,10 +163,10 @@ Once all fields have been processed the runtime makes a final pass to propagate Hopefully we've given you a bit of insight into how the library works under the hood. The other documents on this site go into exhaustive detail of the different features and how to use them but since you're here: -- The library targets [`netstandard2.0`](https://docs.microsoft.com/en-us/dotnet/standard/net-standard). +- The library targets [`netstandard2.0`](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) and `net6.0`. - Out of the box there are no external dependencies beyond official Microsoft packages. -* Every core component and all middleware components required to complete the tasks outlined in this document are referenced through dependency injection. Any one of them (or all of them) can be overridden and extended to do whatever you want as long as you register them prior to calling `.AddGraphQL()` at startup. +* Every core component and all [middleware components](../reference/middleware.md) required to complete the tasks outlined in this document are referenced through dependency injection. Any one of them (or all of them) can be overridden and extended to do whatever you want as long as you register them prior to calling `.AddGraphQL()` at startup. - Inject your own `IGraphResponseWriter` to serialize your results to XML or CSV. - Build your own `IOperationComplexityCalculator` to intercept and alter how a query plan generates its [complexity values](../execution/malicious-queries) to be more suitable to your needs. @@ -238,10 +174,10 @@ Hopefully we've given you a bit of insight into how the library works under the ## Architectural Diagrams -📌  [Structural Diagrams](../assets/2021-01-graphql-aspnet-structural-diagrams.pdf) +📌  [Structural Diagrams](../assets/2022-10-graphql-aspnet-structural-diagrams.pdf) -A set of diagrams outlining the major interfaces and classes that make up GraphQL Asp.Net. +A set of diagrams outlining the major interfaces and classes that make up GraphQL Asp.Net. -📌  [Execution Diagrams](../assets/2021-01-graphql-aspnet-execution-diagrams.pdf) +📌  [Execution Diagrams](../assets/2022-10-graphql-aspnet-execution-diagrams.pdf) A set of flowcharts and relational diagrams showing how various aspects of the library fit together at run time, including the query execution and field execution pipelines. diff --git a/docs/advanced/middleware.md b/docs/reference/middleware.md similarity index 60% rename from docs/advanced/middleware.md rename to docs/reference/middleware.md index 983125e..bed54ad 100644 --- a/docs/advanced/middleware.md +++ b/docs/reference/middleware.md @@ -6,18 +6,20 @@ sidebar_label: Pipelines & Middleware At the heart of GraphQL ASP.NET are 4 middleware pipelines; chains of components executed in a specific order to produce a result. -- `Query Execution Pipeline` : Invoked once per request this pipeline is responsible for validating the incoming package on the POST or GET request, parsing the data and executing a query plan. -- `Field Execution Pipeline` : Invoked once per requested field, this pipeline attempts to generate the requested data by calling the various controller actions and property resolvers. -- `Field Authorization Pipeline`: Ensures the user on the request can perform the action requested. This pipeline is invoked once for the whole query or for each field depending on your schema's configuration. +- `Query Execution Pipeline` : Invoked once per request this pipeline is responsible for validating the incoming package on the POST or GET request, parsing the data and generating a query plan. +- `Field Execution Pipeline` : Invoked once per requested field, this pipeline processes a single field resolver. +- `Schema Item Security Pipeline`: Ensures the user on the request can access a given schema item (field, directive etc.). - `Directive Execution Pipeline`: Executes directives for various phases of schema and query document lifetimes. +All four pipelines can be extended or reworked to include custom components and perform additional work. A call to `.AddGraphQL()` returns a builder that can be used to restructure the pipelines when necessary. + ## Creating New Middleware -Each new middleware component must implement one of the foud middleware interfaces depending on the type of component you are creating; much in the way you'd define a middleware component for ASP.NET. The four middleware interfaces are: +Each new middleware component must implement one of the four middleware interfaces depending on the type of component you are creating; much in the way you'd define a middleware component for ASP.NET. The four middleware interfaces are: - `IQueryExecutionMiddleware` - `IFieldExecutionMiddleware` -- `IFieldAuthorizationMiddleware` +- `ISchemaItemSecurityMiddleware` - `IDirectiveExecutionMiddleware` The interfaces define one method, `InvokeAsync`, with identical signatures save for the type of data context accepted by each. @@ -62,55 +64,61 @@ Each pipeline can be extended using the `SchemaBuilder` returned from calling `. ```csharp // Startup.cs -public void ConfigureServices(IServiceCollection services) -{ - // obtain a reference to the builder after adding - // graphql for your schema - var schemaBuilder = services.AddGraphQL(options => - { - options.ExecutionOptions.MaxQueryDepth = 15; - }); - // add new middleware components to any pipeline - schemaBuilder.QueryExecutionPipeline.AddMiddleware(); -} +// obtain a reference to the builder after adding +// graphql for your schema +var schemaBuilder = services.AddGraphQL(options => + { + options.ExecutionOptions.MaxQueryDepth = 15; + }); + +// add new middleware components to any pipeline +schemaBuilder.QueryExecutionPipeline.AddMiddleware(); + ``` Instead of adding to the end of the existing pipeline you can also call `.Clear()` to remove the default components and rebuild the pipeline from scratch. See below for the list of default middleware components and their order of execution. This can be handy when needing to inject a new component into the middle of the execution chain. +> Modifying the pipeline component order can cause unwanted side effects, including breaking the library such that it no longer functions. Take care when adding or removing middleware components. + ## The Context Object Each context object has specific data fields required for it to perform its work (detailed below). However, all contexts share a common set of items to govern the flow of work. +- `OperationRequest`: The original request being executed. Contains the query text, variables etc. - `Messages`: A collection of messages that will be added to the query result. - `Cancel()`: Marks the context as cancelled and sets the `IsCancelled` property to true. It is up to each middleware component to interpret the meaning of cancelled for its own purposes. A canceled field execution context, for instance, will be discarded and not rendered to the output whereas a canceled query context may or may not generate a result depending on when its cancelled. - `IsValid`: Determines if the context is in a valid and runnable state. Most middleware components will not attempt to process the context if its not in a valid state and will simply forward the request on. By default, a context is automatically invalidated if an error message is added with the `Critical` severity. -- `User`: The ClaimsPrincipal provided by ASP.NET containing the active user's credentials. May be null if user authentication is not setup for your application. +- `SecurityContext`: The information received from ASP.NET containing the credentials of the active user. May be null if user authentication is not setup for your application. - `Metrics`: The metrics package performing any profiling of the query. Various middleware components will stop/start phases of execution using this object. If metrics are not enabled this object will be null. - `Items`: A key/value pair collection of items. This field is developer driven and not used by the runtime. -- `Logger`: An `IGraphLogger` instance scoped to the the current query. +- `Logger`: An `IGraphEventLogger` instance scoped to the the current query. -#### Middleware is served from the DI Container +## Middleware is served from the DI Container Each pipeline is registered as a singleton instance in your service provider but the components within the pipeline are invoked according to the service lifetime you supply when you register them allowing you to setup dependencies as necessary. > Register your middleware components with the `Singleton` lifetime scope whenever possible. -It is recommended that your middleware components be singleton in nature if possible. The two field pipelines can be invoked many dozens (or hundreds) of times per request and fetching new middleware instances for each invocation could impact performance. The internal pipeline manager will retain references to any singleton middleware components once they are generated and avoid this bottleneck whenever possible. All default components are registered as a singletons. +It is recommended that your middleware components be singleton in nature if possible. The field execution and item authorization pipelines can be invoked many dozens of times per request and fetching new middleware instances for each invocation can impact performance. The internal pipeline manager will retain references to any singleton middleware components once they are generated and avoid this bottleneck whenever possible. Most default components are registered as a singletons. ## Query Execution Pipeline The query execution pipeline is invoked once per request. It is supplied with the raw query text from the user and orchestrates the necessary calls to generate a a valid GraphQL result than can be returned to the client. It contains 9 components, in order of execution they are: -1. `ValidQueryRequestMiddleware` : Ensures that the data request recieved is valid and runnable (i.e. was a request provided, is query text defined etc.). +1. `ValidateQueryRequestMiddleware` : Ensures that the data request recieved is valid and runnable (i.e. was a request provided, is query text defined etc.). 2. `RecordQueryMetricsMiddleware`: Governs the query profiling for the context. It will start the recording and terminate it after all other components have completed their operations. -3. `QueryPlanCacheMiddleware` : When the query cache is enabled for the schema this component will analyze the incoming query text and attempt to fetch a pre-cached query plan from storage. +3. `QueryPlanCacheMiddleware` : When the query cache is enabled for the schema, this component will analyze the incoming query text and attempt to fetch a pre-cached query plan from storage. 4. `ParseQueryPlanMiddleware`: When required, this component will lex/parse the query text into a usable document from which a query plan can be created. -5. `GenerateQueryPlanMiddleware`: When required, this component will attempt to generate a fully qualified query plan for its target schema using a parsed document on the context. -6. `AssignQueryOperationMiddleware` : Marries the operation on the request with the operations in the active query plan and selects the appropriate one to be invoked. -7. `AuthorizeQueryOperationMiddleware`: If the schema is configured for `PerRequest` authorization this component will invoke the field authorization pipeline for each field of the selected operation that has security requirements and assign authorization results as appropriate. -8. `ExecuteQueryOperationMiddleware` : Uses the active operation and dispatches field execution contexts to resolve each field of the operation. -9. `PackageQueryResultMiddleware`: Performs a final set of checks on the resolved field data and generates an `IGraphOperationResult` for the query. +5. `ValidateQueryDocumentMiddleware`: Performs a first pass validation of the query document, before any directives are applied. +6. `AssignQueryOperationMiddleware` : Marries the operation name requested with the matching operation in the query document. +7. `ValidationOperationVariableDataMiddleware`: Validates the supplied variables values against those required by the chosen operation. +8. `AuthorizeQueryOperationMiddleware`: If the schema is configured for `PerRequest` authorization this component will invoke the authorization pipeline for each field of the selected operation that has security requirements and assign authorization results as appropriate. +9. `ApplyExecutionDirectivesMiddleware`: Applies all execution directives, if any, to the chosen operation. +10. `GenerateQueryPlanMiddleware`: When required, this component will attempt to generate a fully qualified query plan for its target schema using the chosen operation. +11. `ExecuteQueryOperationMiddleware` : Uses the query plan to dispatches field execution contexts to resolve needed each field. +12. `PackageQueryResultMiddleware`: Performs a final set of checks on the resolved field data and generates an `IGraphOperationResult` for the query. +document on the context. #### GraphQueryExecutionContext @@ -119,16 +127,13 @@ In addition to the common properties defined above, the query execution context ```csharp public class GraphQueryExecutionContext { - public IGraphOperationRequest Request { get; } public IGraphOperationResult Result { get; set; } public IGraphQueryPlan QueryPlan { get; set; } public IList FieldResults { get; } - // common properties omitted for brevity + // other properties omitted for brevity } ``` - -- `Request`: The raw request data received on the HTTP request. Provides access to the query text, requested operation and any passed variable data. - `Result`: The created `IGraphOperationResult`. This property will be null until the result is created. - `QueryPlan`: the created (or retrieved from cache) query plan for the current query. - `FieldResults`: The individual, top-level data fields resolved for the selected operation. These fields are eventually packaged into the result object. @@ -138,9 +143,9 @@ public class GraphQueryExecutionContext The field execution pipeline is executed once for each field of data that needs to be resolved. Its primary job is to turn a request for a field into a data value that can be returned to the client. It contains 5 components, in order of execution they are: 1. `ValidateFieldExecutionMiddleware` : Validates that the context and required invocation data has been correctly supplied. -2. `AuthorizeFieldMiddleware` : If the schema is configured for `PerField` authorization this component will invoke the field authorization pipeline for the current field and assign authorization results as appropriate. -3. `InvokeFieldResolverMiddleware` : The field resolver is called and a data value is created for the active context. This middleware component is ultimately responsible for invoking your controller actions. It also handles call outs to the directive execution pipeline when required. -4. `ProcessChildFieldsMiddleware` : If any child fields were registered with the invocation context for this field they are dispatched using the context's field result as the new source object. +2. `AuthorizeFieldMiddleware` : If the schema is configured for `PerField` authorization this component will invoke the item authorization pipeline for the current field and assign authorization results as appropriate. +3. `InvokeFieldResolverMiddleware` : The field resolver is called and a data value is created for the active context. This middleware component is ultimately responsible for invoking your controller actions. +4. `ProcessChildFieldsMiddleware` : If any child fields are registered for this field they are executing using the context's field result as the new source object. #### GraphFieldExecutionContext @@ -152,48 +157,48 @@ public class GraphFieldExecutionContext public IGraphFieldRequest Request { get; } public object Result { get; set; } - // common properties omitted for brevity + // other properties omitted for brevity } ``` - `Request`: The field request containing any source data, a reference to the metadata for the field as defined by the schema and a reference to the invocation requirements determined by the query plan. - `Result`: The raw data object produced from the field resolver. This value is passed as the source value to any child fields. -## Field Authorization Pipeline +## Schema Item Authorization Pipeline The field authorization pipeline can be invoked as part of query execution or field execution depending on your schema's configuration. It contains 1 component: -1. `FieldSecurityRequirementsMiddleware` : Gathers the authentication and authorization requirements for the given field and ensures that the field _can_ be authorized. There are some instances where by - nested authorization requirements create a scenario in which no user could ever be authorized. This generally involves using multiple auth providers with specific authentication scheme requirements. -2. `FieldAuthenticationMiddleware` : Authenticates the request to the field. This generates a ClaimsPrincipal to be authorized against. -3. `FieldAuthorizationMiddleware`: Inspects the active `ClaimsPrincipal` against the security requirements of the field on the context and generates a `FieldAuthorizationResult` indicating if the user is authorized or not. This component makes no decisions in regards to the authorization state. It is up to the other pipelines to act on the authorization results that are generated. +1. `SchemItemSecurityRequirementsMiddleware` : Gathers the authentication and authorization requirements for the given schema item and ensures that the item _can_ be authorized. +2. `SchemaItemAuthenticationMiddleware` : Authenticates the request to the field. This generates a ClaimsPrincipal to be authorized against if one is not already assigned. +3. `SchemaItemAuthorizationMiddleware`: Inspects the active `ClaimsPrincipal` against the security requirements of the schema item and generates a `SchemaItemSecurityChallengeResult` indicating if the user is authorized or not. This component makes no decisions against the authorization state. It is up to the other pipelines to act on the authorization results in an appropriate manner. -#### GraphFieldAuthorizationContext +#### GraphSchemaItemSecurityChallengeContext In addition to the common properties defined above the field security context defines a number of useful properties: ```csharp - public class GraphFieldSecurityContext + public class GraphSchemaItemSecurityChallengeContext { - public FieldSecurityRequirements SecurityRequirements {get; set;} - public IGraphFieldSecurityRequest Request { get; } - public FieldSecurityChallengeResult Result { get; set; } + public SchemaItemSecurityRequirements SecurityRequirements {get; set;} + public IGraphSchemaItemSecurityRequest Request { get; } + public SchemaItemSecurityChallengeResult Result { get; set; } // common properties omitted for brevity } ``` - `SecurityRequirements`: The security rules that need to be checked to authorize a user. -- `Request`: Contains details about the field currently being authed. -- `Result`: The generated challenge result indicating if the user is authorized or unauthorized for the field. This result will contain additional detailed information as to why a request was not authorized. This information is automatically added to any generated log events. +- `Request`: Contains details about the item currently being authorized. +- `Result`: The generated challenge result indicating if the user is authorized to the item. This result will contain additional detailed information as to why a request was not authorized. This information is automatically added to any generated log events. ## Directive Execution Pipeline -The directive execution pipeline will be invoked for each directive applied to each schema item during schema generation and each time the query engine encounters a -directive at runtime. The directive pipeline contains two components by default: +The directive execution pipeline will be invoked for each directive applied to each schema item during schema generation and each time the query engine encounters a directive on a query document. The directive pipeline contains four components by default: -1. `ValidateDirectiveExecutionMiddleware`: Inspects the execution context against the validation requirements of the given execution phase applying appropriate error messages as necessary. -2. `InvokeDirectiveResolverMiddleware`: Generates a `DirectiveResolutionContext` and invokes the directive's resolver, calling the correct action methods. +1. `ValidateDirectiveExecutionMiddleware`: Inspects the context against the validation requirements for directives and applies appropriate error messages as necessary. +2. `AuthorizeDirectiveMiddleware`: If the schema is configured for `PerField` authorization this component will invoke the item authorization pipeline for the current directive and assign authorization results as appropriate. +3. `InvokeDirectiveResolverMiddleware`: Generates a `DirectiveResolutionContext` and invokes the directive's resolver, calling the correct action methods. +4. `LogDirectiveExecutionMiddleware`: Generates appropriate log messages depending on the directive invoked. #### GraphDirectiveExecutionContext diff --git a/docs/advanced/query-caching.md b/docs/reference/query-caching.md similarity index 52% rename from docs/advanced/query-caching.md rename to docs/reference/query-caching.md index 63d4d97..fee0995 100644 --- a/docs/advanced/query-caching.md +++ b/docs/reference/query-caching.md @@ -4,15 +4,15 @@ title: Query Caching sidebar_label: Query Caching --- -When GraphQL ASP.NET parses a query, it generates a query plan that contains all the required data needed to complete any operation defined on the query document. For most queries this process is near instantaneous but in some schemas it may take an extra moment to generate a full query plan. The query cache will help alleviate this bottleneck by caching a plan for a set period of time to skip the parsing and generation phases when completing a request. +When GraphQL ASP.NET parses a query, it generates a query plan that contains all the required data needed to execute the requested operation. For most queries this process is near instantaneous but in some particularly large queries it may take an extra moment to generate a full query plan. The query cache will help alleviate this bottleneck by caching a plan for a set period of time to skip the parsing and generation phases when completing a request. -The query cache makes a concerted effort to only cache plans that are truly unique and thus it will take a moment to analyze the incoming query to see if it identical to one that is already cached. For small queries the amount of time it takes to scrub the query text and look up a plan in the cache could be _as long_ as reparsing the query (200-300 microseconds). +The query cache makes a concerted effort to only cache plans that are truly unique and thus it will take a moment to analyze the incoming query to see if it identical to one that is already cached. For small queries the amount of time it takes to scrub the query text and look up a plan in the cache could be _as long_ as reparsing the query (200-300μs). Consider using the Query Cache only if: - Your application's individual query size is regularly more than 1000 characters in length - You make use of a lot of interface graph types and a lot of object graph types for each of those interfaces. -- [Profiling](../execution/metrics) reveals a bottleneck in the `parsing` phase of any given query. +- When [Profiling](../execution/metrics) reveals a bottleneck in the `parsing` phase of any given query. ## Enabling the Query Cache @@ -28,4 +28,4 @@ public void ConfigureServices(IServiceCollection services) } ``` -**Note:** Because a query plan contains function pointers to method resolvers and references to local types the default query cache is currently restricted to being in-process for a single server instance. Query Plan serialization for a shared cache such as Redis is on the road map after v1.0 is complete. If you would like to contribute in this area please reach out! +**Note:** Because a query plan contains function pointers and references to local types, the default query cache is currently restricted to being in-process for a single server instance. Query Plan serialization for a shared cache such as Redis is on the road map after v1.0 is complete. If you would like to contribute in this area please reach out! diff --git a/docs/reference/schema-configuration.md b/docs/reference/schema-configuration.md index 8976edb..8744975 100644 --- a/docs/reference/schema-configuration.md +++ b/docs/reference/schema-configuration.md @@ -4,7 +4,7 @@ title: Schema Configuration sidebar_label: Schema Configuration --- -This document contains a list of each property available during schema configuration when a call to `AddGraphQL()` is made when your application starts: +This document contains a list of various configuration settings available during schema configuration. ```csharp // Startup.cs @@ -29,14 +29,15 @@ public void Configure(IApplicationBuilder appBuilder) ## Builder Options -### AddController, AddDirective, AddGraphType, AddType +### AddAssembly ```csharp // usage examples -schemaOptions.AddGraphType(); -schemaOptions.AddController(); +schemaOptions.AddAssembly(assembly); ``` -Adds the single entity of a given type the schema. Use these methods to add individual graph types, directives or controllers. `AddType` acts a catch all and will try to infer the expected action to take against the supplied type. The other entity specific methods will throw an exception should an unqualified type be supplied. For example, trying to supply a controller to `.AddGraphType()` will result in an exception. + +The runtime will scan the referenced assembly and auto-add any found required entities (controllers, types, enums, directives etc.) to the schema. + ### AddSchemaAssembly @@ -49,14 +50,16 @@ When declaring a new schema with .`AddGraphQL()`, the runtime will scan This method has no effect when using `AddGraphQL()`. -### AddAssembly +### AddType* + +Multiple Options: `AddGraphType`, `AddController`, `AddDirective`, `AddType` ```csharp // usage examples -schemaOptions.AddAssembly(assembly); +schemaOptions.AddGraphType(); +schemaOptions.AddController(); ``` - -The runtime will scan the referenced assembly and auto-add any found required entities (controllers, types, enums, directives etc.) to the schema. +Adds a single entity of a given type the schema. Use these methods to add individual graph types, directives or controllers. `AddType` acts a catch all and will try to infer the expected action to take against the supplied type. The other entity-specific methods will throw an exception should an unqualified type be supplied. For example, trying to supply a controller to `.AddGraphType()` will result in an exception. ### ApplyDirective @@ -68,17 +71,17 @@ schemaOptions.ApplyDirective("deprecated") Allows for the runtime registration of a type system directive to a given schema item. See the [directives](../advanced/directives.md#applying-type-system-directives) for complete details on how to use this method. -### AutoRegisterLocalGraphEntities +### AutoRegisterLocalEntities ```csharp // usage examples -schemaOptions.AutoRegisterLocalGraphEntities = true; +schemaOptions.AutoRegisterLocalEntities = true; ``` | Default Value | Acceptable Values | | ------------- | ----------------- | | `true` | `true`, `false` | -When true, those graph entities (controllers, types, enums etc.) that are declared in the entry assembly for the application are automatically registered to the schema. Typically this is your API project where `Startup.cs` is declared. +When true, the graph entities (controllers, types, enums etc.) that are declared in the application entry assembly for the application are automatically registered to the schema. Typically this is your API project where `Startup.cs` or `Program.cs` is declared. ## Authorization Options @@ -102,6 +105,22 @@ Controls how the graphql execution pipeline will authorize a request. ## Declaration Options +### AllowedOperations + +```csharp +// usage examples +schemaOptions.DeclarationOptions.AllowedOperations.Remove(GraphOperationType.Mutation); +``` + +| Default Value | Acceptable Values | +| ------------- | ----------------- | +| `Query, Mutation` | `Query`, `Mutatation`, `Subscription` | + +Controls which top level operations are available on your schema. In general, this property is managed internally and you do not need to alter it. An operation not in the list will not be configured at start up. + +_Note: Subscriptions are automatically added when the subscription library is configured._ + + ### DisableIntrospection ```csharp // usage examples @@ -112,7 +131,9 @@ schemaOptions.DeclarationOptions.DisableIntrospection = false; | ------------- | ----------------- | | `false` | `true`, `false` | -When `true`, any attempts to perform an introspection query by making references to the fields `__schema` and `__type` will fail, preventing exposure of type meta data. +When `true`, any attempts to perform an introspection query by making references to the fields `__schema` and `__type` will fail, preventing exposure of type meta data. + +_Note: Many tools, IDEs and client libraries will fail if you disable introspection data._ ### FieldDeclarationRequirements ```csharp @@ -124,15 +145,15 @@ schemaOptions.DeclarationOptions.FieldDeclarationRequirements = TemplateDeclarat | ----------------------------------------- | ----------------- | | `TemplateDeclarationRequirements.Default` | _all enum values_ | -Indicates to the runtime which fields and values of POCO classes must be explicitly declared in order to be added to a schema when said POCO class is added. +Indicates to the runtime which fields and values of POCO classes must be explicitly declared for them to be added to a schema. By default: -- All enum values will be included. +- All values declared on an `enum` will be included. - All properties of POCOs and interfaces will be included. -- All methods of POCOs and interfaces will be excluded +- All methods of POCOs and interfaces will be excluded. -\* _Controller and Directive action methods are not effected by this setting_ +_NOTE: Controller and Directive action methods are not effected by this setting. Any_ `[GraphField]` _declaration will automatically override these settings._ ### GraphNamingFormatter @@ -154,6 +175,20 @@ _Default formats for the three different entity types_ ## Execution Options +### DebugMode + +```csharp +// usage examples +schemaOptions.ExecutionOptions.DebugMode = false; +``` + +| Default Value | Acceptable Values | +| ------------- | ----------------- | +| `false` | `true`, `false` | + +When true, each field and each list member of each field will be executed sequentially with no parallelization. All asynchronous methods will be individually awaited and allowed to throw immediately. A single encountered exception will halt the entire query process. This can be very helpful in preventing a jumping debug cursor. This option will greatly impact performance and can cause inconsistent query results if used in production. It should only be enabled for [debugging](../development/debugging). + + ### EnableMetrics ```csharp // usage examples @@ -166,72 +201,76 @@ schemaOptions.ExecutionOptions.EnableMetrics = false; When true, metrics and query profiling will be enabled for all queries processed for a given schema. -### QueryTimeout +_Note: This option DOES NOT control if those metrics are sent to the query requestor, just that they are recorded. See [ExposeMetrics](./schema-configuration#exposemetrics) in the response options for that switch. + +### MaxQueryComplexity ```csharp // usage examples -schemaOptions.ExecutionOptions.QueryTimeout = TimeSpan.FromMinutes(2); +schemaOptions.ExecutionOptions.MaxQueryComplexity = 50.0f; ``` -| Default Value | Acceptable Values | -| ------------- | -------------------------- | -| null | Minimum of 10 milliseconds | +| Default Value | Acceptable Values | +| ------------- | -------------------- | +| -_not set_- | Float Greater Than 0 | -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. +The maximum allowed [complexity](../execution/malicious-queries#query-complexity) value of a query. If a query is scored higher than this value it will be rejected. -### DebugMode +### MaxQueryDepth ```csharp // usage examples -schemaOptions.ExecutionOptions.DebugMode = false; +schemaOptions.ExecutionOptions.MaxQueryDepth = 15; ``` -| Default Value | Acceptable Values | -| ------------- | ----------------- | -| `false` | `true`, `false` | +| Default Value | Acceptable Values | +| ------------- | ---------------------- | +| -_not set_- | Integer Greater than 0 | -When true, each field and each list member of each field will be executed sequentially rather than asynchronously. All asynchronous methods will be individually awaited and allowed to throw immediately. A single encountered exception will halt the entire query process. This can be very helpful in preventing a jumping debug cursor as query branches are normally executed in parallel. This option will greatly impact performance and can cause inconsistent query results if used in production. It should only be enabled for [debugging](../development/debugging). +The maximum allowed [field depth](../execution/malicious-queries#maximum-allowed-field-depth) of any child field within a given query. If a query contains a child that is nested deeper than this value the query will be rejected. -### ResolverIsolation + +### QueryTimeout ```csharp // usage examples -schemaOptions.ExecutionOptions.ResolverIsolation = ResolverIsolationOptions.ControllerActions | ResolverIsolation.Properties; +schemaOptions.ExecutionOptions.QueryTimeout = TimeSpan.FromMinutes(2); ``` -| Default Value | -| ------------- | -| `ResolverIsolation.None` | +| Default Value | Acceptable Values | +| ------------- | -------------------------- | +| -_not set_- | > 10 milliseconds | -Resolver types identified in `ResolverIsolation` are guaranteed to be executed independently. This is different than `DebugMode`. In Debug mode a single encountered error will end the request whereas errors encountered in isolated resolvers will still be aggregated. This allows the returning partial results which can be useful in some use cases. +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. The minimum allowed amount of time for a query to run is 10ms. -### MaxQueryDepth +### ResolverIsolation ```csharp // usage examples -schemaOptions.ExecutionOptions.MaxQueryDepth = 15; +schemaOptions.ExecutionOptions.ResolverIsolation = ResolverIsolationOptions.ControllerActions | ResolverIsolation.Properties; ``` -| Default Value | Acceptable Values | -| ------------- | ---------------------- | -| -_not set_- | Integer Greater than 0 | +| Default Value | +| ------------- | +| `ResolverIsolation.None` | -The maximum allowed [field depth](../execution/malicious-queries) of any child field within a given query. If a child is nested deeper than this value the query will be rejected. +Resolver types identified in `ResolverIsolation` are guaranteed to be executed independently. This is different than `DebugMode`. In debug mode a single encountered error will end the request whereas errors encountered in isolated resolvers will still be aggregated. This allows the returning partial results which can be useful in some use cases. -### MaxQueryComplexity +## Response Options + +### AppendServerHeader ```csharp // usage examples -schemaOptions.ExecutionOptions.MaxQueryComplexity = 50.0f; +schemaOptions.ResponseOptions.AppendServerHeader = true; ``` -| Default Value | Acceptable Values | -| ------------- | -------------------- | -| -_not set_- | Float Greater Than 0 | +| Default Value | Acceptable Values | +| ------------- | ----------------- | +| `true` | `true`, `false` | -The maximum allowed [complexity](../execution/malicious-queries) value of a query. If a query is scored higher than this value it will be rejected. +When true, an `X-GraphQL-AspNet-Server` header with the current library version (e.g. `v1.0.1`) is added to the outgoing response. This option has no effect when a custom `HttpProcessorType` is declared. -## Response Options ### ExposeExceptions @@ -244,7 +283,9 @@ schemaOptions.ResponseOptions.ExposeExceptions = false; | ------------- | ----------------- | | `false` | `true`, `false` | -When true, exception details including message, type and stack trace will be sent to the requestor as part of any error messages. Setting this value to true can expose sensitive server details and is considered a security risk. Disable in any production environments. +When true, exception details including message, type and stack trace will be sent to the requestor as part of any error messages. + +> WARNING: Setting this value to true can expose sensitive server details and is considered a security risk. ### ExposeMetrics @@ -259,18 +300,20 @@ schemaOptions.ResponseOptions.ExposeMetrics = false; When true, the full Apollo trace gathered when a query is executed is sent to the requestor. This value is disregarded unless `ExecutionOptions.EnableMetrics` is set to true. -### AppendServerHeader +_Note: Metrics data for large queries can be quite expansive; double or tripling the size of the json data returned._ + +### IndentDocument ```csharp // usage examples -schemaOptions.ResponseOptions.AppendServerHeader = true; +schemaOptions.ResponseOptions.ExposeExceptions = true; ``` | Default Value | Acceptable Values | | ------------- | ----------------- | -| `true` | `true`, `false` | +| `true` | `true`, `false` | -When true, an `X-PoweredBy` header is added to the outgoing response to indicate it was generated from graphql. This option has no effect when a custom `HttpProcessorType` is declared. +When true, the default json response writer will indent and "pretty up" the output response to make it more human-readable. Turning off this setting can result in a smaller output response. ### MessageSeverityLevel @@ -282,7 +325,7 @@ schemaOptions.ResponseOptions.AppendServerHeader = GraphMessageSeverity.Informat | ---------------------------------- | -------------------------------------- | | `GraphMessageSeverity.Information` | \-_any `GraphMessageSeverity` value_\- | -Indicates which messages generated during a query should be sent to the requestor. Any message with a value at or above the provided level will be delivered. +Indicates which messages generated during a query should be sent to the requestor. Any message with a value at or higher than the provided level will be delivered. ### TimeStampLocalizer @@ -293,50 +336,39 @@ schemaOptions.ResponseOptions.TimeStampLocalizer = (dtos) => dtos.DateTime; | Func | | --------------------------------- | -| `(dtOffset) => dtOffset.DateTime` | +| `(dtoffset) => dtoffset.DateTime` | -A function to convert any timestamps present in the output into a value of a given timezone. By default no localization occurs and all times are delivered in their native `UTC-0`. This localizer does not effect any query data values, only those messaging related components. +A function to convert any timestamps present in the output into a value of a given timezone. By default, no localization occurs and all times are delivered in their native `UTC-0` format. This localizer does not effect any query field date values, only those related to internal messaging. ## QueryHandler Options -### DisableDefaultRoute +### AuthenticatedRequestsOnly ```csharp // usage examples -schemaOptions.QueryHandler.DisableDefaultRoute = false; +schemaOptions.QueryHandler.AuthenticatedRequestsOnly = false; ``` | Default Value | Acceptable Values | | ------------- | ----------------- | | `false` | `true`, `false` | -When set to true the default handler and query processor will not be registered with the ASP.NET runtime when the application starts. - -### Route - -```csharp -// usage examples -schemaOptions.QueryHandler.Route = "/graphql"; -``` - -| Default Value | -| ------------- | -| `/graphql` | +When true, only those requests that are successfully authenticated by the ASP.NET runtime will be passed to GraphQL. Should an unauthenticated request make it to the graphql query processor it will be immediately rejected. This setting has no effect when a custom `HttpProcessorType` is declared. -Represents the REST end point where GraphQL will listen for new POST requests. In multi-schema configurations this value will need to be unique per schema type. -### AuthenticatedRequestsOnly +### DisableDefaultRoute ```csharp // usage examples -schemaOptions.QueryHandler.AuthenticatedRequestsOnly = false; +schemaOptions.QueryHandler.DisableDefaultRoute = false; ``` | Default Value | Acceptable Values | | ------------- | ----------------- | | `false` | `true`, `false` | -When true, only those requests that are successfully authenticated by the ASP.NET runtime will be passed to GraphQL. Should an unauthenticated request make it to the graphql query processor it will be immediately rejected. This option has no effect when a custom `HttpProcessorType` is declared. +When set to true the default route and http query processor will **NOT** be registered with the ASP.NET runtime when the application starts. GraphQL queries will not be processed unless manually invoked. + ### HttpProcessorType @@ -349,98 +381,113 @@ schemaOptions.QueryHandler.HttpProcessorType = typeof(MyProcessorType); | ------------- | | `null` | -When set to a type, GraphQL will attempt to load the provided type from the configured DI container in order to handle graphql requests. Any class wishing to act as an Http Processor must implement `IGraphQLHttpProcessor`. In most cases it may be easier to extend `DefaultGraphQLHttpProcessor`. +When set to a Type, GraphQL will attempt to load the provided type from the configured DI container in order to handle graphql requests. Any class wishing to act as an Http Processor must implement `IGraphQLHttpProcessor`. In most cases it may be easier to extend `DefaultGraphQLHttpProcessor`. + +### Route + +```csharp +// usage examples +schemaOptions.QueryHandler.Route = "/graphql"; +``` + +| Default Value | +| ------------- | +| `/graphql` | + +Represents the REST end point where GraphQL will listen for new POST and GET requests. In multi-schema configurations this value will need to be unique per schema type. ## Subscription Server Options -These options are available to configure a subscription server for a given schema via `.AddSubscriptions(serverOptions)` or `.AddSubscriptionServer(serverOptions)` +These options are available to configure a subscription server for a given schema via `.AddSubscriptions(serverOptions)` -### DisableDefaultRoute + +### AuthenticatedRequestsOnly ```csharp // usage examples -serverOptions.DisableDefaultRoute = false; +serverOptions.AuthenticatedRequestsOnly = false; ``` + | Default Value | Acceptable Values | | ------------- | ----------------- | -| `false ` | `true`, `false` | - -When true, GraphQL will not register a component to listen for web socket requests. You must handle the acceptance of web sockets yourself and inform the subscription server that a new one exists. If you wish to implement your own web socket middleware handler, viewing [DefaultGraphQLHttpSubscriptionMiddleware](https://github.com/graphql-aspnet/graphql-aspnet/blob/master/src/graphql-aspnet-subscriptions/Defaults/DefaultGraphQLHttpSubscriptionMiddleware.cs) may help. +| `false` | `true`, `false` | +When true, only requests that are successfully authenticated by the ASP.NET runtime will be passed to GraphQL and registered as a subscription client. Connections with unauthenticated sources are immediately closed. -### Route +### ConnectionKeepAliveInterval -Similar to the query/mutation query handler route this represents the path the default subscription middleware will look for when accepting new web sockets +The interval at which the subscription server will send a protocol-specific message to a connected graphql client informing it the connection is still open. ```csharp // usage examples -serverOptions.Route = "/graphql"; +serverOptions.ConnectionKeepAliveInterval = TimeSpan.FromMinutes(2); ``` | Default Value | | ------------- | -| `/graphql` | +| `2 minutes` | +_Note: Not all messaging protocols support message level keep alives._ -Represents the http end point where GraphQL will listen for new requests. In multi-schema configurations this value will need to be unique per schema type. -### HttpMiddlewareComponentType +### ConnectionInitializationTimeout -The middleware component GraphQL will inject into the ASP.NET pipeline to intercept new web socket connection requests. +When supported by a messaging protocol, represents a timeframe after the connection is initiated in which a successful initialization handshake must occur. ```csharp // usage examples -serverOptions.HttpMiddlewareComponentType = typeof(MyMiddleware); +serverOptions.ConnectionInitializationTimeout = TimeSpan.FromSeconds(30); ``` | Default Value | | ------------- | -| `null` | +| `30 seconds` | -When null, `DefaultGraphQLHttpSubscriptionMiddleware` is used. -### KeepAliveInterval +_Note: Not all messaging protocols require an explicit timeframe or support an inititalization handshake._ + +### DefaultMessageProtocol -The interval at which the subscription server will send a message to a connected client informing it the connection is still open. +When set, represents a valid and supported messaging protocol that a client should use if it does not specify which protocols it can communicate in. ```csharp // usage examples -serverOptions.KeepAliveInterval = TimeSpan.FromMinutes(2); +serverOptions.DefaultMessageProtocol = "my-custom-protocol"; ``` | Default Value | | ------------- | -| `2 minutes` | +| `null` | -This is a different keep alive than the websocket-level keep alive interval. The default apollo subscription server implementation uses this value to know when to send its "CONNECTION_KEEP_ALIVE" message type to a client. +_Note: By default, this value is not set and connected clients MUST supply a prioritized protocol list._ -### MessageBufferSize - -The size of the message buffer, in bytes used to extract and deserialize a message being received from a connected client. +### DisableDefaultRoute ```csharp // usage examples -serverOptions.MessageBufferSize = 4 * 1024; +serverOptions.DisableDefaultRoute = false; ``` +| Default Value | Acceptable Values | +| ------------- | ----------------- | +| `false ` | `true`, `false` | + +When true, GraphQL will not register a component to listen for web socket requests. You must handle the acceptance of web sockets yourself and provision client proxies that can interact with the runtime. If you wish to implement your own web socket middleware handler, viewing [DefaultGraphQLHttpSubscriptionMiddleware](https://github.com/graphql-aspnet/graphql-aspnet/blob/master/src/graphql-aspnet-subscriptions/Defaults/DefaultGraphQLHttpSubscriptionMiddleware.cs) may help. -| Default Value | -| ------------- | -| `4kb` | -### MaxConcurrentClientNotifications +### HttpMiddlewareComponentType -The maximum number of connected clients a server will attempt to communicate with at one time. +When set, represents the custom middleware component GraphQL will inject into the ASP.NET pipeline to intercept new web socket connection requests. ```csharp // usage examples -serverOptions.MaxConcurrentClientNotifications = 50; +serverOptions.HttpMiddlewareComponentType = typeof(MyMiddleware); ``` -| Default Value | Minimum Value | -| ------------- | ----------------- | -| `50` | `1` | +| Default Value | +| ------------- | +| `null` | -If for instance, there are 100 connected clients, all of which are subscribed to the same event, the subscription server will attempt to communicate new data to at most 50 of them at one time with remaining clients being queued and notified as the original 50 acknowledge the event. This can help throttle resources and prevent a subscription server from being overloaded. +When null, `DefaultGraphQLHttpSubscriptionMiddleware` is used. ### RequireAuthenticatedConnection @@ -460,51 +507,37 @@ When set to true, the subscription middleware will immediately reject any websoc When set to false, the subscription middleware will initially accept all web socket requests. +### Route -## Global Configuration Settings -Global settings effect the entire server instance, they are not restricted to a single schema registration. Instead they should be set before calling `.AddGraphQL()` -```csharp -// Startup.cs -public void ConfigureServices(IServiceCollection services) -{ - // setup any global configuration options before - // calling AddGraphQL() - GraphQLProviders.GlobalConfiguration.CONFIG_OPTION_NAME +Similar to the query/mutation query handler route this represents the path the default subscription middleware will look for when accepting new web sockets. - services.AddGraphQL(); -} -``` -### ControllerServiceLifetime ```csharp -GraphQLProviders.GlobalConfiguration.ControllerServiceLifetime = ServiceLifetime.Transient +// usage examples +serverOptions.Route = "/graphql"; ``` +| Default Value | +| ------------- | +| `/graphql` | + -| Default Value | Acceptable Values | -| ------------- | ----------------- | -| `Transient ` | `Transient`, `Scoped`, `Singleton` | +Represents the http end point where GraphQL will listen for new web socket requests. In multi-schema configurations this value will need to be unique per schema type. + +### SupportedMessageProtocols -The configured service lifetime is what all controllers and directives will be registered as within the DI container during schema setup. +When populated, represents a list of messaging protocol keys supported by this schema. A connected client MUST be able to communicate in one of the approved +values or it will be dropped. ```csharp -// Startup.cs -public void ConfigureServices(IServiceCollection services) -{ - // All controllers will be registered as Scoped - GraphQLProviders.GlobalConfiguration.ControllerServiceLifetime = ServiceLifetime.Scoped; - services.AddGraphQL(); -} +// usage examples +var myProtocols = new Hashset(); +myProtocols.Add("protocol1"); +myProtocols.Add("protocol2"); +serverOptions.SupportedMessageProtocols = myProtocols; ``` -If you need to register only one or two controllers as a different scope add them to the DI container prior to calling `.AddGraphQL()` -```csharp -// Startup.cs -public void ConfigureServices(IServiceCollection services) -{ - // MyController will be registered as Scoped - services.AddScoped(); +| Default Value | +| ------------- | +| `null` | - // all other controllers will be registered as Transient - services.AddGraphQL(); -} -``` \ No newline at end of file +_Note: By default, this setting is null, meaning any server supported protocol will be usable by the target schema. If set to an empty set, then the schema is effectively disabled as no supported protocols will be matched._ diff --git a/docs/types/scalars.md b/docs/types/scalars.md index 76b66b2..06d3fe5 100644 --- a/docs/types/scalars.md +++ b/docs/types/scalars.md @@ -35,7 +35,7 @@ GraphQL ASP.NET has 20 built in scalar types. ## Input Value Resolution -When a value is resolved it's read from the query document (or variable collection) in one of three ways: +When a value is resolved, it's read from the query document (or variable collection) in one of three ways: - **String** : A string of characters, delimited by `"quotes"` - **Boolean** The value `true` or `false` with no quotes @@ -47,7 +47,7 @@ Scalars used as input arguments require that any supplied value match at least o ## Scalar Names Are Fixed -Unlike other graph types, scalar names are fixed across all schemas. The name defined above (including casing), is how they appear in your schema's introspection queries. These names conform to the accepted standard for graphql type names. +Unlike other graph types, scalar names are fixed across all schemas. The name defined above (including casing), is how they appear in your schema's introspection queries. These names conform to the accepted standard for graphql type names. This is true for any custom scalars you may build as well. #### Nullable\ diff --git a/website/pages/en/index.js b/website/pages/en/index.js index 9a810f7..266102b 100644 --- a/website/pages/en/index.js +++ b/website/pages/en/index.js @@ -31,7 +31,7 @@ class HomeSplash extends React.Component {

{GraphQL ASP.NET} {/*{siteConfig.tagline}*/} - v0.12.2-beta + v0.13.0-beta

); diff --git a/website/sidebars.json b/website/sidebars.json index 4c791ba..52d4bb3 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -26,18 +26,23 @@ "types/scalars", "types/list-non-null" ], - "Logging": ["logging/structured-logging", "logging/standard-events"], - "Query Execution": ["execution/metrics", "execution/malicious-queries"], "Advanced": [ "advanced/subscriptions", "advanced/type-expressions", "advanced/directives", "advanced/custom-scalars", "advanced/graph-action-results", - "advanced/middleware", - "advanced/query-cache", "advanced/multi-schema-support" ], + "Logging": [ + "logging/structured-logging", + "logging/standard-events", + "logging/subscription-events" + ], + "Query Execution": [ + "execution/metrics", + "execution/malicious-queries" + ], "Development Concerns": [ "development/debugging", "development/unit-testing", @@ -46,10 +51,13 @@ "References": [ "reference/how-it-works", "reference/schema-configuration", + "reference/global-configuration", "reference/attributes", "reference/graph-controller", "reference/graph-directive", "reference/http-processor", + "reference/middleware", + "reference/query-cache", "reference/demo-projects", "reference/performance" ] @@ -57,4 +65,4 @@ "docs-other": { "First Category": [] } -} +} \ No newline at end of file