diff --git a/docs/advanced/directives.md b/docs/advanced/directives.md index 98c28fb..3ca4939 100644 --- a/docs/advanced/directives.md +++ b/docs/advanced/directives.md @@ -4,75 +4,131 @@ title: Directives sidebar_label: Directives --- -Directives are implemented in much the same way as `GraphController` but where you'd indicate a a graph controller method as being for a query or mutation, directives must indicate where they can be declared and when they should execute. +> Directives were completely reimagined in June 2022, this document represents the new approach to directives. + +Directives are implemented in much the same way as a `GraphController` but where you'd indicate an action method as being for a query or mutation, directive action methods must indicate the location(s) they can be applied in either a query document or the type system. + +```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) + { + return ifArgument ? this.Cancel() : this.Ok(); + } + } +``` ## Anatomy of a Directive All directives must: - Inherit from `GraphQL.AspNet.Directives.GraphDirective` -- Provide at least one appropriate life cycle action method definition +- Provide at least one action method that indicates at least 1 valid `DirectiveLocation`. All directive action methods must: - Share the same method signature -- Return `IGraphActionResult` +- Return `IGraphActionResult` or `Task` -This is the code for the built in `@include` directive: + +### Helpful Properties + +The following properties are available to all directive action methods: + +* `this.DirectiveTarget` - The targeted schema item or resolved field value depending the directive type. +* `this.Request` - The invocation request for the currently executing directive. Contains lots of advanced information just as execution phase, the executing location etc. + + +### Directive Arguments + +Directives can 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. ```csharp - [GraphType("include")] - [DirectiveLocations(ExecutableDirectiveLocation.AllFieldSelections)] - public sealed class IncludeDirective : GraphDirective - { - public IGraphActionResult BeforeFieldResolution([FromGraphQL("if")] bool ifArgument) - { - return ifArgument ? this.Ok() : this.Cancel(); - } + public class MyValidDirective : GraphDirective + { + [DirectiveLocations(DirectiveLocation.FIELD)] + public IGraphActionResult Execute(int arg1, string arg2) { /.../ } + + [DirectiveLocations(DirectiveLocation.FRAGMENT_SPREAD)] + public Task Execute(int arg1, string arg2) { /.../ } + } + + public class MyInvalidDirective : GraphDirective + { + // method parameters MUST match for all directive action methods. + [DirectiveLocations(DirectiveLocation.FIELD)] + public IGraphActionResult Execute(int arg1, int arg2) { /.../ } + + [DirectiveLocations(DirectiveLocation.FRAGMENT_SPREAD)] + public IGraphActionResult Execute(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. -This Directive: -- Declares its name using the `[GraphType]` attribute - - The name will be derived from the class name if the attribute is omitted -- Defines that it can be included in a query document at all applicable field selection locations using the `[DirectiveLocations]` attribute - - This is the default behavior and will be set automatically if the attribute is omitted. -- Declares a life cycle method of `BeforeFieldResolution` and provides one required input argument -- Uses the `[FromGraphQL]` attribute to declare the input argument's name in the schema +### Returning Data from a Directive -### Directive Life Cycle Methods +Directives can't directly return data or resolve a field. They can only indicate success or failure. The following helper methods can help to quickly generate an appropriate `IGraphActionResult`. -GraphQL ASP.NET offers two points in the field pipeline at which a directive could be invoked. By declaring your methods as one of these two method names (or both), you can invoke the directive at that point in the lifecycle: +* `this.Ok()`: + * The directive executed correctly and processing of the current schema item or target field should continue. +* `this.Cancel()`: + * The directive failed and the schema should not be generated or the target field should be dropped. -- `BeforeFieldResolution` - - This method is executed before a controller method is called. -- `AfterFieldResolution` - - The method is executed after the controller method is called. +> Throwing an exception within an action method of a directive will cause the current query to fail completely. Use `this.Cancel()` to discard only the currently resolving field. Normal nullability validation rules still apply. -> Directive life cycle methods must have an identical signature +### Directive Target +The `this.DirectiveTarget` property will contain either an `ISchemaItem` for type system directives or the resolved field value for execution directives. This value is useful in performing additional operations such as extending a field resolver during schema generation or taking further action against a resolved field. -While a directive may declare both life cycle methods independently, it is only a single entity in a schema. As a result both life cycle methods must share a common signature (i.e. the same input parameters and return type). The runtime will throw an exception when your schema is created if the signatures differ. +### Directive Lifecycle Phases -### Returning Data from a Directive +Each directive is executed in one of three phases as indicated by `this.DirectivePhase`: -Directives can't directly return data or resolve a field but they can influence the field data using `this.Request.DataSource`. This property gives you direct access to the data items the field is currently resolving as well as the resolved values and current item state if executing `AfterFieldResolution`. You're free to manipulate this data as necessary and can change the resolved value, cancel a specific item etc. +* `SchemaGeneration` + * The directive is being applied during schema generation. `this.DirectiveTarget` will be the `ISchemaItem` targeted by the directive. You can make any necessary changes to the schema item during this phase. Once all type system directives have been applied the schema is read-only and should not be changed. +* `BeforeFieldResolution` + * The directive is currently executing BEFORE its target field is resolved. `this.DirectiveTarget` will be null. You must explicitly tell a directive to execute before field resolution via the `[DirectiveInvocation]` attribute. By default this phase will be skipped. +* `AfterFieldResolution` + * The directive is currently executing AFTER its target field is resolved. `this.DirectiveTarget` will contain the resolved field value. This value can be freely edited and the result will be used as the field result. You are responsible for ensuring type consistancy. Altering the concrete data type of `DirectiveTarget` may cause a query to fail or yield unpredictable results. -**Directives Must Return IGraphActionResult** +## Execution Directives -Since directives don't directly return data, they must always return a `IGraphActionResult` to fully declare their intent. In the include directive, we're returning `this.Ok()` and `this.Cancel()` which allows us to affect the pipeline processing status, signaling for it to continue or cancel. +### Example: @include Directive -## How Directives are Executed +This is the code for the built in `@include` directive: -Directives are executed as a middleware component in the field execution pipeline. If the request supplies any directives to be ran, they are executed in the pipeline and depending on the result, the pipeline is allowed to continue or not. +```csharp + [GraphType("include")] + public sealed class IncludeDirective : GraphDirective + { + [DirectiveLocations(DirectiveLocation.FIELD | DirectiveLocation.FRAGMENT_SPREAD | DirectiveLocation.INLINE_FRAGMENT)] + public IGraphActionResult Execute([FromGraphQL("if")] bool ifArgument) + { + return ifArgument ? this.Ok() : this.Cancel(); + } + } +``` -The include directive above is very simple. Depending on the input argument it either returns `this.Ok()` indicating everything executed fine and processing of the pipeline should continue, or it returns `this.Cancel()` to end the request. The include directive does not attempt to influence or filter the incoming data items, its an "all or nothing" directive. +This Directive: -## Directive Execution Order +- Declares its name using the `[GraphType]` attribute + - The name will be derived from the class name if the attribute is omitted +- Defines that it can be included in a query document at all applicable field selection locations using the `[DirectiveLocations]` attribute + - This is similar to using the `[Query]` or `[Mutation]` attributes for a controller method. + - In addition to defining where the directive can be used this attribute also indicates which action method is invoked at that location. You can use multiple action methods as long as they have the same signature. +- 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. +- Is executed once for each field or fragment field its applied to in a query document -When more than one directive is encountered for a single field they are executed in sequence in the order declared in the source text. No directive has precedence over another and all will be executed. +> The action method name `Execute` in this example is arbitrary. Method names can be whatever makes the most sense to you. -## Working with Fragments +### Directive Execution Order + +When more than one directive is encountered for a single field, they are executed in the order encountered, from left to right, in the source text. + +### Working with Fragments Directives attached to spreads and named fragments are executed for each of the top level fields in the fragment. @@ -100,20 +156,315 @@ fragment donutData on Donut @directiveB { The directives executed, in order, for each field are: -- **`allPasteries`**: `@directive1` +- **`allPasteries`**: @directive1 -- **`flavor`**: `@directiveA`, `@directiveB`, `@directiveC` +- **`flavor`**: @directiveA -> @directiveB -> @directiveC -* **`size`**: `@directiveA`, `@directiveB` +* **`size`**: @directiveA -> @directiveB -* **`length`**: `@directiveD` +* **`length`**: @directiveD * **`id`, `name`, `width`**: _-no directives-_ Since the `donutData` fragment is spread into the `allPastries` field its directives are also spread into the fields at the "top-level" of the fragment. -## Sharing Data with Fields +### Sharing Data with Fields It is recommended that your directives act independently and be self contained. But if your use case calls for a need to share data with the fields they are targeting, the key-value pair collection `Items` that can be used: - `this.Request.Items` is a collection scoped to the current field execution. These values are available to all executing directives as well as the field resolver within the current pipeline. + + +## Type System Directives +### Example: @toUpper + +This directive will extend the resolver of a field to turn any strings into upper case letters. + +```csharp + public class ToUpperDirective : GraphDirective + { + [DirectiveLocations(DirectiveLocation.FIELD_DEFINITION)] + public IGraphActionResult Execute() + { + // ensure we are working with a graph field definition and that it returns a string + var field = this.DirectiveTarget as IGraphField; + if (field != null) + { + // ObjectType represents the .NET Type of the data returned by the field + if (field.ObjectType != typeof(string)) + throw new Exception("This directive can only be applied to string fields"); + + // update the resolver to execute the orignal + // resolver then apply upper caseing to the string result + var resolver = field.Resolver.Extend(ConvertToupper); + item.UpdateResolver(resolver); + } + + return this.Ok(); + } + + private static Task ConvertToupper(FieldResolutionContext context, CancellationToken token) + { + if (context.Result is string) + context.Result = context.Result?.ToString().ToUpper(); + + return Task.CompletedTask; + } + } +``` + +This Directive: + +* Targets any FIELD_DEFINITION. +* Ensures that the target field returns returns a string. +* Extends the field's resolver to convert the result to an upper case string. +* The directive is executed once per field its applied to when the schema is created. The extension method is executed on every field resolution. + * If an exception is thrown the schema will fail to create and the server will not start. + * if the action method returns a cancel result (e.g. `this.Cancel()`) the schema will fail to create and the server will not start. + +### Example: @deprecated + +```csharp + public sealed class DeprecatedDirective : GraphDirective + { + [DirectiveLocations(DirectiveLocation.FIELD_DEFINITION | DirectiveLocation.ENUM_VALUE)] + public IGraphActionResult Execute([FromGraphQL("reason")] string reason = "No longer supported") + { + if (this.DirectiveTarget is IGraphField field) + { + field.IsDeprecated = true; + field.DeprecationReason = reason; + } + else if (this.DirectiveTarget is IEnumValue enumValue) + { + enumValue.IsDeprecated = true; + enumValue.DeprecationReason = reason; + } + + return this.Ok(); + } + } +``` + + +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 and enum value its applied to when the schema is created. + +### Applying Type System Directives + +#### Using the `[ApplyDirective]` attribute + +If you have access to the source code of a given type and want to apply a directive to it directly use the `[ApplyDirective]` attribute: + +
+
+ +```csharp +// Person.cs +public class Person +{ + [ApplyDirective(typeof(ToUpperDirective))] + public string Name{ get; set; } +} +``` + +
+
+ +```javascript +// GraphQL Type Definition Equivilant +type Person { + name: String @toUpper +} +``` + +
+
+
+
+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. + + +
+
+ +```csharp +// Person.cs +[ApplyDirective("monitor")] +public class Person +{ + public string Name{ get; set; } +} +``` + +
+
+ +```javascript +// GraphQL Type Definition Equivilant +type Person @monitor { + name: String +} +``` + +
+
+ +
+
+**Adding Arguments with [ApplyDirective]** + +Arguments added to the apply directive attribute will be passed to the directive in the order they are encountered. The supplied values must be coercable into the expected data types for an input parameters. + +
+
+ +```csharp +// Person.cs +public class Person +{ + [ApplyDirective( + "deprecated", + "Names don't matter")] + public string Name{ get; set; } +} +``` + +
+
+ +```javascript +// GraphQL Type Definition Equivilant +type Person { + name: String @deprecated("Names don't matter") +} +``` + +
+
+ +
+ +#### Using Schema Options + +Alternatively, instead of using attributes to apply directives you can apply directives during schema configuration: + +
+
+ +```csharp +// startup.cs +public void ConfigureServices(IServiceCollection services) +{ + // other code ommited for brevity + + services.AddGraphQL(options => + { + options.AddGraphType(); + + // mark Person.Name as deprecated + options.ApplyDirective("monitor") + .ToItems(schemaItem => + schemaItem.IsObjectGraphType()); + } +} +``` + +
+
+ +```javascript +// GraphQL Type Definition Equivilant +type Person @monitor { + name: String +} +``` + +
+
+ +
+ +> 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 the order of declaration with the `[ApplyDirective]` attributes taking precedence over the `.ApplyDirective()` method. + +**Adding arguments via .ApplyDirective()** + +Adding Arguments via schema options is a lot more flexible than via the apply directive attribute. 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. + + +
+
+ +```csharp +// startup.cs +public void ConfigureServices(IServiceCollection services) +{ + // other code ommited for brevity + + services.AddGraphQL(options => + { + options.AddGraphType(); + options.ApplyDirective("deprecated") + .WithArguments("Names don't matter") + .ToItems(schemaItem => + schemaItem.IsGraphField("name")); + } +} +``` + + +
+
+ +```javascript +// GraphQL Type Definition Equivilant +type Person { + name: String @deprecated("Names don't matter") +} +``` + +
+
+ + +
+ +## 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 objects and properties 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 (transient, scoped, singleton). +* The directive can be instantiated with any dependencies or services you wish; making for a much richer experience. + +## Directive Security +Directives are not considered a layer of security by themselves. Instead, they are invoked within the security context of their applied target: + +* **Execution Directives** - Execute in the same context as the field to which they are applied. If the requestor can resolve the field, they can also execute the directives attached to that field. + +* **Type System Directives** - Are implicitly trusted and executed without a `ClaimsPrincipal` while the schema is being built. No additional security is applied to type system directives. + +> WARNING: Only use type system directives that you trust. They will always be executed when applied to one or more schema items. + +## Understanding the Type System +GraphQL ASP.NET builds your schema and all of its types from your controllers and objects. In general, you do not need to interact with it, however; when applying type system directives you are affecting the final generated schema at run time, making changes as you see fit. When doing this you are forced to interact with the internal type system. + +**UML Diagrams** + +These [uml diagrams](../assets/2022-05-graphql-aspnet-type-system-interface-diagrams.pdf) details 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. See the [full source code](https://github.com/graphql-aspnet/graphql-aspnet/tree/master/src/graphql-aspnet/Configuration/SchemaItemExtensions.cs) for details. + +## 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 diff --git a/docs/advanced/middleware.md b/docs/advanced/middleware.md index e3cf708..c51153c 100644 --- a/docs/advanced/middleware.md +++ b/docs/advanced/middleware.md @@ -4,19 +4,21 @@ title: Pipelines and Custom Middleware sidebar_label: Pipelines & Middleware --- -At the heart of GraphQL ASP.NET are 3 middleware pipelines; chains of components executed in a specific order to produce a result. +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 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. +- `Directive Execution Pipeline`: Executes directives for various phases of schema and query document lifetimes. ## Creating New Middleware -Each new middleware component must implement one of the three middleware interfaces depending on the type of component you are creating; much in the way you'd define a filter or middleware component for ASP.NET MVC. The three middleware interfaces are: +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: - `IQueryExecutionMiddleware` - `IFieldExecutionMiddleware` - `IFieldAuthorizationMiddleware` +- `IDirectiveExecutionMiddleware` The interfaces define one method, `InvokeAsync`, with identical signatures save for the type of data context accepted by each. @@ -54,18 +56,6 @@ public class MyQueryMiddleware : IQueryExecutionMiddleware } ``` -## 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. - -- `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. -- `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. - ## Registering New Middleware Each pipeline can be extended using the `SchemaBuilder` returned from calling `.AddGraphQL()` at startup. Each schema that is added to GraphQL will generate its own builder with its own set of pipelines and components. They can be configured independently as needed. @@ -88,6 +78,19 @@ public void ConfigureServices(IServiceCollection services) 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. + +## 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. + +- `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. +- `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. + #### 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. @@ -180,3 +183,26 @@ In addition to the common properties defined above the field authorization conte - `Request`: Contains the field metadata for this context, including the security rules that need to be checked. - `Result`: The generated authorization 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. + + +## 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: + +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. + + +#### GraphDirectiveExecutionContext +```csharp +public class GraphDirectiveExecutionContext +{ + public IGraphDirectiveRequest Request { get; } + public IDirective Directive {get;} + + // common properties omitted for brevity +} +``` + +- `Request`: Contains the directive metadata for this context, including the DirectiveTarget, execution phase and executing location. +- `Directive`: The specific `IDirective`, registered to the schema, that is being processed. \ No newline at end of file diff --git a/docs/assets/2022-05-graphql-aspnet-type-system-interface-diagrams.pdf b/docs/assets/2022-05-graphql-aspnet-type-system-interface-diagrams.pdf new file mode 100644 index 0000000..6d665f7 Binary files /dev/null and b/docs/assets/2022-05-graphql-aspnet-type-system-interface-diagrams.pdf differ diff --git a/docs/reference/demo-projects.md b/docs/reference/demo-projects.md index f93237d..3fe3523 100644 --- a/docs/reference/demo-projects.md +++ b/docs/reference/demo-projects.md @@ -20,6 +20,11 @@ Demonstrates fields with authorization requirements and how access denied messag Demonstrates how to setup a [Firebase](https://firebase.google.com/) project and link a GraphQL ASP.NET project to it. +📌  [Custom Directives](https://github.com/graphql-aspnet/demo-projects/tree/master/Custom-Directives) + +Demostrates creating and applying a type system directive and a custom execution directive. + + 📌  [Subscriptions w/ Azure Service Bus](https://github.com/graphql-aspnet/demo-projects/tree/master/Subscriptions-AzureServiceBus) Demonstrates the use of an external subscription event publisher and a consumer to deserialize and route events. Use of this demo project requires your own [Azure Service Bus](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview) namespace. @@ -27,3 +32,4 @@ Demonstrates the use of an external subscription event publisher and a consumer 📌  [Subscriptions w/ React & Apollo Client](https://github.com/graphql-aspnet/demo-projects/tree/master/Subscriptions-ReactApolloClient) A sample react application that makes use of the [apollo client](https://www.apollographql.com/docs/react/) to connect to a GraphQL ASP.NET server. + diff --git a/docs/types/unions.md b/docs/types/unions.md index 7fe5852..b893f7b 100644 --- a/docs/types/unions.md +++ b/docs/types/unions.md @@ -4,11 +4,9 @@ title: Unions sidebar_label: Unions --- -## Unions - Unions are an aggregate graph type representing multiple, different `OBJECT` types with no guaranteed fields or interfaces in common; for instance, `Salad` or `Bread`. Because of this, unions define no fields themselves but provide a common way to query the fields of the union members when one is encountered. -Unlike other graph types there is no concrete representation of unions. Where a `class` is an object graph type or a .NET enum is an enum graph type there is no analog for unions. Instead unions are virtual types that exist at runtime based their declaration site in a `GraphController`. +Unlike other graph types there is no concrete representation of unions. Where a `class` is an object graph type or a .NET `enum` is an enum graph type there is no analog for unions. Instead unions are semi-virtual types that are created from proxy classes that represent them at design time. ## Declaring a Union @@ -56,9 +54,9 @@ query { In this example we : -- Declared an action method named `RetrieveFood` with a field name of `searchFood` -- Declared a union on our graph named `SaladOrBread` -- Included two object types in the union: `typeof(Salad)` and `typeof(Bread)` +- Declared an action method named `RetrieveFood` with a graph field name of `searchFood` +- Declared a union type on our graph named `SaladOrBread` +- Included two object types in the union: `Salad` and `Bread` Unlike with [interfaces](./interfaces) where the possible types returned from an action method can be declared else where, you MUST provide the types to include in the union in the declaration. @@ -66,7 +64,7 @@ Unlike with [interfaces](./interfaces) where the possible types returned from an ### What to Return for a Union -Notice we have a big question mark on what the action method returns in the above example. From a C# perspective, there is no `IFood` interface shared between `Salad` and `Bread`. This represents a problem for static-typed languages like C#. Since unions are virtual types there exists no common `System.Type` that you can return for generated data. `System.Object` might work but it tends to be too general and the runtime will reject it as a safe guard. +Notice we have a big question mark on what the action method returns in the above example. From a C# perspective, in this example, there is no `IFood` interface shared between `Salad` and `Bread`. This represents a problem for static-typed languages like C#. Since unions are virtual types there exists no common type that you can return for generated data. `System.Object` might work but it tends to be too general and the runtime will reject it as a safe guard. So what do you do? Return an `IGraphActionResult` instead and let the runtime handle the details. @@ -92,11 +90,11 @@ public class KitchenController : GraphController } ``` -Under the hood, GraphQL ASP.NET looks at the returned object type at runtime to evaluate the graph type reference then continues on in that scope with the returned value. +> Any controller action that declares a union MUST return an `IGraphActionResult` ## Union Proxies -If you need to reuse your unions in multiple methods you'll want to create a class that implements `IGraphUnionProxy` (or inherits from `GraphUnionProxy`) to encapsulate the details, then add that as a reference in your controller methods instead of the individual types. This can also be handy for uncluttering your code if you have a lot of possible types for the union. The return type of your method will still need to be `IGraphActionResult`. You cannot return a `IGraphUnionProxy` as a value. +In the example above we declare the union inline. But what if we wanted to reuse the `SaladOrBread` union in multiple places. You could declare the union exactly the same on each method or use a union proxy. Create a class that implements `IGraphUnionProxy` or inherits from `GraphUnionProxy` to encapsulate the details, then add that as a reference in your controller methods instead of the individual types. This can also be handy for uncluttering your code if you have a lot of possible types for the union. The return type of your method will still need to be `IGraphActionResult`. You cannot return a `IGraphUnionProxy` as a value. ```csharp public class KitchenController : GraphController @@ -107,24 +105,27 @@ public class KitchenController : GraphController } // SaladOrBread.cs -using GraphQL.AspNet.Schemas.TypeSystem; public class SaladOrBread : GraphUnionProxy { public SaladOrBread() - : base(typeof(Salad), typeof(Bread)) - {} + : base() + { + this.Name = "SaladOrBread"; + this.AddType(typeof(Salad)); + this.AddType(typeof(Bread)); + } } ``` -> You can create a union proxy by inheriting from `GraphUnionProxy` or directly implementing `IGraphUnionProxy` +> If you don't supply a name, graphql will automatically use the name of the proxy as the name of the union. ## Union Name Uniqueness -Union names must be unique in a schema. If you do declare a union in multiple action methods without a proxy, GraphQL will attempt to validate the references by name and included types. As long as all declarations are the same, that is the name and the set of types match, then there is no issue. Otherwise, a `GraphTypeDeclarationException` will be thrown. +Union names must be unique in a schema. If you do declare a union in multiple action methods without a proxy, GraphQL will attempt to validate the references by name and included types. As long as all declarations are the same, that is the name and the set of included types, then there graphql will accept the union. Otherwise, a `GraphTypeDeclarationException` will be thrown at startup. ## Liskov Substitutions -[Liskov substitutions](https://en.wikipedia.org/wiki/Liskov_substitution_principle) (the L in [SOLID](https://en.wikipedia.org/wiki/SOLID)) are an important part of object oriented programming and .NET. To be able to have one class masquerade as another allows us to easily extend our code's capabilities without any rework. +[Liskov substitutions](https://en.wikipedia.org/wiki/Liskov_substitution_principle) (the L in [SOLID](https://en.wikipedia.org/wiki/SOLID)) are an important part of object oriented programming. To be able to have one class masquerade as another allows us to easily extend our code's capabilities without any rework. ```csharp @@ -188,15 +189,12 @@ query {
-Most of the time, GraphQL ASP.NET can correctly interpret which type it should match on to allow the query to progress. However, in the above example, we declare a union, `RollOrBread`, that is of types `Roll` and `Bread` yet we return a `Bagel` from the action method. - -Since `Bagel` inherits from `Roll` and subsequently from `Bread` which type should we match against when executing the following query? +Most of the time, graphql can correctly interpret the correct union type of a returned data object and continue processing the query. However, in the above example, we declare a union, `RollOrBread`, that is of types `Roll` and `Bread` yet we return a `Bagel` from the action method. -The bagel is both the type `Roll` AND the type `Bread`, it could be used as either. GraphQL ASP.NET will be unable to determine which type to use and can't advance the query to select the appropriate fields. The query result is said to be indeterminate. +Since `Bagel` is both a `Roll` and `Bread` which type should graphql match against to continue executing the query? Since it could be either, graphql will be unable to determine which type to use and can't advance the query to select the appropriate fields. The query result is said to be indeterminate. -GraphQL ASP.NET offers a way to allow you to take control of your unions and make the determination on your own. The `ResolveType` method of `IGraphUnionProxy` will be called whenever a query result is indeterminate, allowing you to choose which of your UNION's allowed types should be used. +GraphQL ASP.NET offers a way to allow you to take control of your unions and make the determination on your own. The `MapType` method of `GraphUnionProxy` will be called whenever a query result is indeterminate, allowing you to choose which of your UNION's allowed types should be used. -> Note: `IGraphUnionProxy.ResolveType` is not based on the explicit value being inspected, but only on the `System.Type`. The results for a given field are cached for speedier type resolution on subsequent queries. ```csharp // RollOrBread.cs @@ -206,7 +204,7 @@ public class RollOrBread : GraphUnionProxy : base(typeof(Roll), typeof(Bread)) {} - public override Type ResolveType(Type runtimeObjectType) + public override Type MapType(Type runtimeObjectType) { if (runtimeObjectType == typeof(Bagel)) return typeof(Roll); @@ -226,8 +224,11 @@ public class BakeryController : GraphController } ``` +> Note: `MapType` is not based on the resolved field value, but only on the `System.Type`. This is by design to guarantee consistency in query execution. +> If your returned type causes the query to remain indeterminate a validation error (rule 6.4.3) will be applied to the query. + The query will now interpret all `Bagels` as `Rolls` and be able to process the query correctly. If, via your logic you are unable to determine which of your Union's types to return then return null and GraphQL will supply the caller with an appropriate error message stating the query was indeterminate. Also, returning any type other than one that was formally declared as part of your Union will result in the same indeterminate state. -**Note:** Most of the time GraphQL ASP.NET will never call the `ResolveType` method on your UNION. If your union types do not share an inheritance chain, for instance, the method will never be called. If your types do share an inheritance chain, such as in the example above, considering using an interface graph type along with specific fragments instead of a UNION, to avoid the issue altogether. +**Note:** Most of the time GraphQL ASP.NET will never call the `TypeMapper` on your UNION. If your union types do not share an inheritance chain, for instance, the method will never be called. If your types do share an inheritance chain, such as in the example above, considering using an interface graph type along with specific fragments instead of a UNION, to avoid the issue altogether. diff --git a/website/pages/en/index.js b/website/pages/en/index.js index 3cb247f..5b96a88 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.9.2-beta + v0.10.0-beta

);