diff --git a/docs/advanced/custom-scalars.md b/docs/advanced/custom-scalars.md index f5afe3a..bcdc550 100644 --- a/docs/advanced/custom-scalars.md +++ b/docs/advanced/custom-scalars.md @@ -13,7 +13,7 @@ This can be done for any value that can be represented as a simple set of charac Lets say we wanted to build a scalar called `Money` that can handle both an amount and currency symbol. We might accept it in a query like this: -```csharp +```csharp title="Declaring a Money Scalar" public class InventoryController : GraphController { [QueryRoot("search")] @@ -49,11 +49,11 @@ query { ``` -The query supplies the data as a quoted string, `"$18.45"`, but our action method receives a `Money` object. Internally, GraphQL senses that the value should be `Money` from the schema definition and invokes the correct resolver to parse the value and generate the .NET object that can be passed to our action method. +The query supplies the data as a quoted string, `"$18.45"`, but our action method receives a `Money` object. Internally, GraphQL senses that the supplied string value should be `Money` from the schema definition and invokes the correct resolver to parse the value and generate the .NET object that can be passed to our action method. ## Implement IScalarGraphType -To create a scalar graph type we need to implement `IScalarGraphType` and register it with GraphQL. The methods and properties of `IScalarGraphType` are as follows: +To create a scalar we need to implement `IScalarGraphType` and register it with GraphQL. The methods and properties of `IScalarGraphType` are as follows: ```csharp title="IScalarGraphType.cs" public interface IScalarGraphType @@ -61,14 +61,16 @@ public interface IScalarGraphType string Name { get; } string InternalName { get; } string Description { get; } + string SpecifiedByUrl { get; } TypeKind Kind { get; } bool Publish { get; } ScalarValueType ValueType { get; } Type ObjectType { get; } TypeCollection OtherKnownTypes { get; } ILeafValueResolver SourceResolver { get; } - IScalarValueSerializer Serializer { get; } + object Serialize(object item); + string SerializeToQueryLanguage(object item); bool ValidateObject(object item); } @@ -76,11 +78,6 @@ public interface ILeafValueResolver { object Resolve(ReadOnlySpan data); } - -public interface IScalarValueSerializer -{ - object Serialize(object item); -} ``` ### IScalarGraphType Members @@ -91,17 +88,26 @@ public interface IScalarValueSerializer - `Kind`: Scalars must always be declared as `TypeKind.SCALAR`. - `Publish`: Indicates if the scalar should be published for introspection queries. Unless there is a very strong reason not to, scalars should always be published. Set this value to `true`. - `ValueType`: A set of flags indicating what type of source data, read from a query, this scalar is capable of processing (string, number or boolean). GraphQL will do a preemptive check and if the query document does not supply the data in the correct format it will not attempt to resolve the scalar. Most custom scalars will use `ScalarValueType.String`. +- `SpecifiedByUrl`: A url, formatted as a string, pointing to information or the specification that defines this scalar. (optional, can be null) - `ObjectType`: The primary, internal type representing the scalar in .NET. In our example above we would set this to `typeof(Money)`. - `OtherKnownTypes`: A collection of other potential types that could be used to represent the scalar in a controller class. For instance, integers can be expressed as `int` or `int?`. Most scalars will provide an empty list (e.g. `TypeCollection.Empty`). - `SourceResolver`: An object that implements `ILeafValueResolver` which can convert raw input data into the scalar's primary `ObjectType`. -- `Serializer`: An object that implements `IScalarValueSerializer` that converts the internal representation of the scalar (a class or struct) to a valid, serialized output (a number, string or boolean). -- `ValidateObject(object)`: A method used when validating data returned from a a field resolver. GraphQL will call this method and provide the value from the resolver to determine if its acceptable and should continue resolving child fields. - -> `ValidateObject(object)` should not attempt to enforce nullability rules. In general, all scalars should return `true` if the provided object is `null`. +- `Serialize(object)`: A method that converts an instance of your scalar to a leaf value that is serializable in a query response + - This method must return a `number`, `string`, `bool` or `null`. + - When converting to a number this can be any C# number value type (int, float, decimal etc.). +- `SerializeToQueryLanguage(object)`: A method that converts an instance of your scalar to a string representing it if it were declared as part of a schema language type definition. + - This method is used when generated default values for field arguments and input object fields via introspection queries. + - This method must return a value exactly as it would appear in a schema type definition For example, strings must be surrounded by quotes. + +- `ValidateObject(object)`: A method used when validating data returned from a a field resolver. GraphQL will call this method and provide an object instance to determine if its acceptable and can be used in a query. + +:::note + `ValidateObject(object)` should not attempt to enforce nullability rules. In general, all scalars should return `true` for a validation result if the provided object is `null`. +::: ### ILeafValueResolver Members -- `Resolve(ReadOnlySpan)`: A resolver function capable of converting an array of characters into the internal representation of the type. +- `Resolve(ReadOnlySpan)`: A resolver function capable of converting an array of characters into the internal representation of the scalar. #### Dealing with Escaped Strings @@ -113,7 +119,7 @@ Example string data: - `"""triple quoted string"""` - `"With \"\u03A3scaped ch\u03B1racters\""`; -The `StringScalarType` provides a handy static method for unescaping the data if you don't need to do anything special with it, `StringScalarType.UnescapeAndTrimDelimiters`. +The static type `GraphQLStrings` provides a handy static method for unescaping the data if you don't need to do anything special with it, `GraphQLStrings.UnescapeAndTrimDelimiters`. Calling `UnescapeAndTrimDelimiters` with the previous examples produces: @@ -123,35 +129,13 @@ Calling `UnescapeAndTrimDelimiters` with the previous examples produces: #### Indicating an Error -When resolving input values with `Resolve()`, if the provided value is not usable and must be rejected then the entire query document must be rejected. For instance, if a document contained the value `"$15.R0"` for our money scalar. Throw an exception and GraphQL will automatically generate a response error with the correct origin information indicating the line and column in the query document where the error occurred. - -If you throw `UnresolvedValueException` your error message will be delivered verbatim to the requestor as a normal error message. GraphQL will obfuscate any other exception type to a generic message and only expose your exception details if allowed by the [schema configuration](../reference/schema-configuration). - -### IScalarValueSerializer Members +When resolving input values with `Resolve()`, if the provided value is not usable and must be rejected then the entire query document must be rejected. For instance, if a document contained the value `"$15.R0"` for our money scalar it would need to be rejected because `15.R0` cannot be converted to a decimal decimal. -- `Serialize(object)`: A serializer that converts the internal representation of the scalar to a [graphql compliant scalar value](https://graphql.github.io/graphql-spec/October2021/#sec-Scalars); a `number`, `string`, `bool` or `null`. - - When converting to a number this can be any number value type (int, float, decimal etc.). +Throw an exception when this happens and GraphQL will automatically generate an appropriate response with the correct origin information indicating the line and column in the query document where the error occurred. However, like with any other encounterd exception, GraphQL will obfuscate it to a generic message and only expose your exception details if allowed by the [schema configuration](../reference/schema-configuration). -> `Serialize(object)` must return a string, any primative number or a boolean. - -Taking a look at the at the serializer for the `Guid` scalar type we can see that while internally the `System.Guid` struct represents the value we convert it to a string when serializing it. Most scalar implementations will serialize to a string. - -```csharp title="GuidScalarSerializer.cs" -public class GuidScalarSerializer : IScalarValueSerializer -{ - public object Serialize(object item) - { - if (item == null) - return item; - - return ((Guid)item).ToString(); - } -} -``` - -> The `Serialize()` method will only be given an object of the approved types for the scalar or null. - ---- +:::tip Pro Tip! +If you throw `UnresolvedValueException` your error message will be delivered verbatim to the requestor as part of the response message instead of being obfuscated. +::: ### Example: Money Scalar The completed Money custom scalar type @@ -176,9 +160,27 @@ The completed Money custom scalar type public TypeCollection OtherKnownTypes => TypeCollection.Empty; - public ILeafValueResolver SourceResolver { get; } = new MoneyLeafTypeResolver(); + public ILeafValueResolver SourceResolver { get; } = new MoneyValueResolver(); + + public object Serialize(object item) + { + if (item == null) + return item; + + var money = (Money)item; + return $"{money.Symbol}{money.Price}"; + } + + public string SerializeToQueryLanguage(object item) + { + // convert to a string first + var serialized = this.Serialize(item); + if (serialized == null) + return "null"; - public IScalarValueSerializer Serializer { get; } = new MoneyScalarTypeSerializer() + // return value as quoted + return $"\"{serialized}\""; + } public bool ValidateObject(object item) { @@ -189,30 +191,19 @@ The completed Money custom scalar type } } - public class MoneyLeafTypeResolver : ILeafValueResolver + public class MoneyValueResolver : ILeafValueResolver { public object Resolve(ReadOnlySpan data) { // example only, more validation code is needed to fully validate // the data - var sanitizedMoney = StringScalarType.UnescapeAndTrimDelimiters(data); + var sanitizedMoney = GraphQLStrings.UnescapeAndTrimDelimiters(data); if(sanitizedMoney == null || sanitizedMoney.Length < 2) throw new UnresolvedValueException("Money must be at least 2 characters"); return new Money(sanitizedMoney[0], Decimal.Parse(sanitizedMoney.Substring(1))); } } - public class MoneyScalarTypeSerializer : IScalarValueSerializer - { - public override object Serialize(object item) - { - if (item == null) - return item; - - var money = (Money)item; - return $"{money.Symbol}{money.Price}"; - } - } ``` ## Registering A Scalar @@ -233,33 +224,43 @@ Since our scalar is represented by a .NET class, if we don't pre-register it Gra ## @specifiedBy Directive -GraphQL provides a special, built-in directive called `@specifiedBy` that allows you to supply a URL pointing to a the specification for your custom scalar. This url is used by various tools to additional data to your customers so they know how to interact with your scalar type. It is entirely optional. +GraphQL provides a special, built-in directive called `@specifiedBy` that allows you to supply a URL pointing to a the specification for your custom scalar. This url is used by various tools to link to additional data for you or your customers so they know how to interact with your scalar type. It is entirely optional. The @specifiedBy directive can be applied to a scalar in all the same ways as other type system directives or by use of the special `[SpecifiedBy]` attribute. -```csharp title="Apply the @specifiedBy" -// apply the directive to a single schema -GraphQLProviders.ScalarProvider.RegisterCustomScalar(typeof(MoneyScalarType)); +```csharp title="Applying the @specifiedBy" +// apply the directive to a single schema at startup services.AddGraphQL(o => { +// highlight-start o.ApplyDirective("@specifiedBy") .WithArguments("https://myurl.com") .ToItems(item => item.Name == "Money"); +// highlight-end }); -// via the ApplyDirective attribute +// via the [ApplyDirective] attribute // for all schemas +// highlight-next-line [ApplyDirective("@specifiedBy", "https://myurl.com")] -public class MoneyScalarType : IScalarType -{ - // ... -} +public class MoneyScalarType : IScalarGraphType +{} -// via the special SpecifiedBy attribute +// via the special [SpecifiedBy] attribute // for all schemas +// highlight-next-line [SpecifiedBy("https://myurl.com")] -public class MoneyScalarType : IScalarType +public class MoneyScalarType : IScalarGraphType +{} + +// as part of the contructor +// for all schemas +public class MoneyScalarType : IScalarGraphType { - // ... + public MoneyScalarType() + { + // highlight-next-line + this.SpecifiedByUrl = "https://myurl.com"; + } } ``` @@ -272,16 +273,13 @@ A few points about designing your scalar: - The runtime will pass a new instance of your scalar graph type to each registered schema. It must be declared with a public, parameterless constructor. - Scalar types should be simple and work in isolation. - The `ReadOnlySpan` provided to `ILeafValueResolver.Resolve` should be all the data needed to generate a value, there should be no need to perform side effects or fetch additional data. -- Scalar types should not track any state or depend on any stateful objects. -- `ILeafValueResolver.Resolve` must be **FAST**! Since your resolver is used to construct an initial query plan from a text document, it'll be called orders of magnitude more often than any other method. +- Scalar types should not track any state, depend on any stateful objects, or attempt to use any sort of dependency injection. +- `ILeafValueResolver.Resolve` must be **FAST**! Since your resolver is used to construct an initial query plan from a text document, it'll be called many orders of magnitude more often than any other method. ### Aim for Fewer Scalars Avoid the urge to start declaring a lot of custom scalars. In fact, chances are that you'll never need to create one. In our example we could have represented our money scalar as an INPUT_OBJECT graph type: -
-
- ```csharp title="Money as an Input Object Graph Type" public class InventoryController : GraphController { @@ -302,9 +300,6 @@ public class Money } ``` -
-
- ```graphql title="Using the Money Input Object" query { search(minPrice: { @@ -316,10 +311,9 @@ query { } ``` -
-
-
This is a lot more flexible. We can add more properties to `Money` when needed and not break existing queries. Whereas with a scalar if we change the acceptable format of the string data any existing query text will now be invalid. It is almost always better to represent your data as an object or input object rather than a scalar. -> Creating a custom scalar should be a last resort, not a first option. +:::caution Be Careful +Creating a custom scalar should be a last resort, not a first option. +::: diff --git a/docs/advanced/directives.md b/docs/advanced/directives.md index 1761cbd..d81fb95 100644 --- a/docs/advanced/directives.md +++ b/docs/advanced/directives.md @@ -5,9 +5,9 @@ sidebar_label: Directives sidebar_position: 2 --- -## What is a directive? +## 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 directives built into graphql: +Directives decorate parts of your schema or a query document to perform some sort of custom logic. What that logic is, is entirely up to you. There are several directives built into graphql: - `@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 @@ -34,12 +34,23 @@ public sealed class SkipDirective : GraphDirective } ``` -All directives must: +```graphql title="Quring using @skip" +# skip including flavor +query { + donut(id: 15) { + id + name + flavor @skip(if: true) + } +} +``` + +✅ All directives must: - Inherit from `GraphQL.AspNet.Directives.GraphDirective` - Provide at least one action method that indicates at least 1 valid `DirectiveLocation`. -All directive action methods must: +✅ All directive action methods must: - Share the same method signature - The input arguments must match exactly in type, name, casing and declaration order. @@ -65,7 +76,7 @@ The following properties are available to all directive action methods: ### 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 the action methods 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. 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 title="Arguments for Directives" public class MyValidDirective : GraphDirective @@ -80,10 +91,12 @@ Directives may contain input arguments just like fields. However, its important public class MyInvalidDirective : GraphDirective { [DirectiveLocations(DirectiveLocation.FIELD)] + // highlight-next-line public IGraphActionResult ExecuteField(int arg1, int arg2) { /.../ } // method parameters MUST match for all directive action methods. [DirectiveLocations(DirectiveLocation.FRAGMENT_SPREAD)] + // highlight-next-line public IGraphActionResult ExecuteFragSpread(int arg1, string arg2) { /.../ } } ``` @@ -94,7 +107,9 @@ Directives may contain input arguments just like fields. However, its important ## Execution Directives -Execution Directives are applied to query documents and executed on the single request in which they are encountered. +(_**a.k.a. Operation Directives**_) + +Execution Directives are applied to query documents and executed only on single request in which they are encountered. ### Example: @include @@ -104,9 +119,7 @@ This is the code for the built in `@include` directive: [GraphType("include")] public sealed class IncludeDirective : GraphDirective { - [DirectiveLocations(DirectiveLocation.FIELD - | DirectiveLocation.FRAGMENT_SPREAD - | DirectiveLocation.INLINE_FRAGMENT)] + [DirectiveLocations(DirectiveLocation.FIELD | DirectiveLocation.FRAGMENT_SPREAD | DirectiveLocation.INLINE_FRAGMENT)] public IGraphActionResult Execute([FromGraphQL("if")] bool ifArgument) { if (this.DirectiveTarget is IIncludeableDocumentPart idp) @@ -123,7 +136,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 schema. - 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. @@ -171,6 +184,7 @@ public class ToUpperDirective : GraphDirective // add a post resolver to the target field document // part to perform the conversion when the query is // ran + // highlight-next-line fieldPart.PostResolver = ConvertToUpper; } @@ -200,10 +214,9 @@ query { } ``` -### Working with Batch Extensions +#### 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 extension declaration. The dictionary is always keyed by source item reference. +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 that owns the field the directive was applied to and `TResult` is the data type or an `IEnumerable` of the data type the target field returns. The dictionary is always keyed by source item reference. :::caution Be Careful with Batch Type Extensions 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]`. @@ -211,6 +224,8 @@ batch extension declaration. The dictionary is always keyed by source item refer ## Type System Directives +(_**a.k.a. Schema Directives**_) + Type System directives are applied to schema items and executed at start up while the schema is being created. ### Example: @toLower @@ -359,8 +374,7 @@ services.AddGraphQL(options => // mark Person.Name as deprecated options.ApplyDirective("monitor") - .ToItems(schemaItem => - schemaItem.IsObjectGraphType()); + .ToItems(schemaItem => schemaItem.IsObjectGraphType()); } ``` @@ -381,18 +395,15 @@ or a `Func` that returns a collection of any parameters y ```csharp title="Apply Directives at Startup With Arguments" -public void ConfigureServices(IServiceCollection services) +// startup code +services.AddGraphQL(options => { - // other code ommited for brevity - - services.AddGraphQL(options => - { - options.AddGraphType(); - options.ApplyDirective("deprecated") - .WithArguments("Names don't matter") - .ToItems(schemaItem => - schemaItem.IsGraphField("name")); - } + options.AddGraphType(); + // highlight-start + options.ApplyDirective("@deprecated") + .WithArguments("Names don't matter") + .ToItems(schemaItem => schemaItem.IsGraphField("name")); + // highlight-end } ``` @@ -418,20 +429,24 @@ public sealed class ScanItemDirective : GraphDirective } // Option 1: Apply the directive to the class directly +// highlight-start [ApplyDirective("@scanItem", "medium")] [ApplyDirective("@scanItem", "high")] +// highlight-end public class Person {} // Option 2: Apply the directive at startup services.AddGraphQL(o => { // ... + // highlight-start o.ApplyDirective("@scanItem") .WithArguments("medium") .ToItems(item => item.IsObjectGraphType()); o.ApplyDirective("@scanItem") .WithArguments("high") .ToItems(item => item.IsObjectGraphType()); + // highlight-end }); ``` @@ -465,6 +480,7 @@ Directives can be secured like controller actions. However, where a controller a 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. ```csharp title="Applying Authorization to Directives" +// highlight-next-line [Authorize(Policy = "admin")] public sealed class UnRedactDirective : GraphDirective { diff --git a/docs/advanced/graph-action-results.md b/docs/advanced/graph-action-results.md index 967bd39..14e95af 100644 --- a/docs/advanced/graph-action-results.md +++ b/docs/advanced/graph-action-results.md @@ -63,7 +63,7 @@ Built in Controller Action Methods: - `this.BadRequest()`: Commonly used in conjunction with `this.ModelState`. This result indicates the data supplied to the method is not valid for the operation. If given the model state collection an error for each validation error is rendered. - `this.InternalServerError()`: Indicates an unintended error, such as an exception occurred. The supplied message will be added to the response and no child fields will be resolved. -## Directive Action Results +### Directive Action Results [Directives](./directives) have two built in Action Results: - `this.Ok()`: Indicates that the directive completed its expected operation successfully and processing can continue. @@ -71,6 +71,34 @@ Built in Controller Action Methods: - If this is a type system directive, the schema will fail to complete and the server will not start. - If this is an execution directive, the query will be abandoned and the user will receive an error message. +## Using an IGraphActionResult + +Using a graph action result is straight forward. Use it like you would a regular action result with a REST query: + +```csharp +public class BakeryController : GraphController +{ + [Query("donut", typeof(Donut))] + public IGraphActionResult RetrieveDonut(int id) + { + if(id < 0) + // highlight-next-line + return this.Error("Invalid Id"); + + Donut donut = new Donut(id); + // highlight-next-line + return this.Ok(donut); + } +} +``` + +Notice, however; that we had to declare the return type of the donut field in the `[Query]` attribute. This is because the actual return type is hidden by the use of `IGraphActionResult`. This is the trade off to the extra functionality provided by action results. Since GraphQL is a statically typed language all field return types must be known at startup. + +:::info +Using a graph action result requires you to declare the return type of your action method elsewhere, usually in the `[Query]` or `[Mutation]` attribute. +::: + + ## Custom Graph Action Results You can add your own custom action results. This can be particularly useful on larger teams where you want a uniform field response or error message contents for a given situation. diff --git a/docs/advanced/multiple-schema.md b/docs/advanced/multiple-schema.md index 0355edd..88bbd72 100644 --- a/docs/advanced/multiple-schema.md +++ b/docs/advanced/multiple-schema.md @@ -5,16 +5,21 @@ sidebar_label: Multi-Schema Support sidebar_position: 5 --- -GraphQL ASP.NET supports multiple schemas on the same server out of the box. Each schema is recognized by the runtime by its concrete type. To register multiple schemas you'll need to create your own type that implements `ISchema` +GraphQL ASP.NET supports multiple schemas on the same server out of the box. Each schema is recognized by its concrete .NET type. -## Implement ISchema +## Create a Custom Schema -While it is possible to implement `ISchema` directly, if you don't require any extra functionality in your schema its easier to just subclass the default schema. +To register multiple schemas you'll need to create your own class that implements `ISchema`. While it is possible to implement `ISchema` directly, if you don't require any extra functionality in your schema its easier to just inherit from the default `GraphSchema`. Updating the `Name` and `Description` is highly encouraged as the information is referenced in several different messages and can be very helpful in debugging. ```csharp title="Declaring Custom Schemas" public class EmployeeSchema : GraphSchema { + // The schema name may be referenced in some error messages + // and log entries. public override string Name => "Employee Schema"; + + // The description is publically available via introspection queries. + public override string Description => "My Custom Schema"; } public class CustomerSchema : GraphSchema @@ -23,12 +28,16 @@ public class CustomerSchema : GraphSchema } ``` + +> Implementing `ISchema` and its dependencies from scratch is not a trivial task and is beyond the scope of this documentation. + + ## Register Each Schema Each schema can be registered using an overload of `.AddGraphQL()` during startup. -```csharp title="Adding A Custom Schema" -services.AddGraphQL(); +```csharp title="Adding A Custom Schema at Startup" +services.AddGraphQL(); ``` ### Give Each Schema its Own HTTP Route @@ -38,12 +47,14 @@ The query handler will attempt to register a schema to `/graphql` as its URL by ```csharp title="Adding Multiple Schemas" services.AddGraphQL((options) => { + // highlight-next-line options.QueryHandler.Route = "/graphql_employees"; // add assembly or graph type references here }); services.AddGraphQL((options) => { + // highlight-next-line options.QueryHandler.Route = "/graphql_customers"; // add assembly or graph type references here }); @@ -52,12 +63,13 @@ services.AddGraphQL((options) => ## Disable Local Graph Entity Registration -You may want to disable the registering of local graph entities (the entities in the startup assembly) on one or both schemas lest you want each schema to contain the same controllers and graph types. +(optional) You may want to disable the registering of local graph entities (the entities in the startup assembly) on one or both schemas lest you want each schema to contain the same controllers and graph types. ```csharp title="Startup Code" // Optionally Disable Local Entity Registration services.AddGraphQL(o => { + // highlight-next-line o.AutoRegisterLocalEntities = false; }); ``` \ No newline at end of file diff --git a/docs/advanced/subscriptions.md b/docs/advanced/subscriptions.md index 9fea240..78e4115 100644 --- a/docs/advanced/subscriptions.md +++ b/docs/advanced/subscriptions.md @@ -13,8 +13,12 @@ Successfully handling subscriptions in your GraphQL server can be straight forwa The first step to using subscriptions is to install the subscription server package. -```powershell - PS> Install-Package GraphQL.AspNet.Subscriptions -AllowPrereleaseVersions +```powershell title="Install The Library" +# Using the dotnet CLI +> dotnet add package GraphQL.AspNet.Subscriptions --prerelease + +# using Package Manager Console +> Install-Package GraphQL.AspNet.Subscriptions -IncludePrerelease ``` This adds the necessary components to create a subscription server for a given schema such as communicating with web sockets, parsing subscription queries and responding to events. @@ -25,22 +29,24 @@ You must configure web socket support for your Asp.Net server instance separatel After web sockets are added to your server, add subscription support to the graphql registration. -```csharp title="Add Subscription Support" - -// configuring services +```csharp title="Add Subscription Support at Startup" +// configuring services at startup +// --------------- services.AddWebSockets(/*...*/); services.AddGraphQL() +// highlight-next-line .AddSubscriptions(); // building the application pipeline +// --------------- app.UseWebSockets(); - +// highlight-next-line app.UseGraphQL(); ``` :::tip - Don't forget to call `.UseWebsockets()` before calling `.UseGraphQL()` + Don't forget to call `.UseWebsockets()` before calling `.UseGraphQL()`. ::: ### Create a Subscription @@ -52,6 +58,7 @@ public class SubscriptionController : GraphController { // other code not shown for brevity + // highlight-next-line [SubscriptionRoot("onWidgetChanged", typeof(Widget), EventName = "WIDGET_CHANGED")] public IGraphActionResult OnWidgetChanged(Widget eventData, string filter){ if(eventData.Name.StartsWith(filter)) @@ -68,11 +75,6 @@ public class SubscriptionController : GraphController } } ``` - -:::info -Subscriptions can be asyncronous and return a `Task` as well. -::: - Here we've declared a new subscription, one that takes in a `filter` parameter to restrict the data that any subscribers receive. A query to invoke this subscription may look like this: @@ -91,7 +93,7 @@ Any updated widgets that start with the phrase "Big" will then be sent to the re ### Publish a Subscription Event -In order for the subscription server to send data to any subscribers it has to be notified when its time to do so. 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 appropriate action method for any subscribers, using the supplied data, and deliver the results to the client. +In order for the subscription server to send data to any subscribers it has to be notified when its time to do so. It does this via named Subscription Events. These are internal, schema-unique keys (strings) that identify when something happened, usually via a mutation. Once the mutation publishes an event, the subscription server will execute the appropriate action method for any subscribers, using the supplied data, and deliver the results to the client. ```csharp title="MutationController.cs" public class MutationController : GraphController @@ -107,6 +109,7 @@ public class MutationController : GraphController // publish a new event to let any subscribers know // something changed + // highlight-next-line this.PublishSubscriptionEvent("WIDGET_CHANGED", widget); return this.Ok(widget); } @@ -114,12 +117,12 @@ public class MutationController : GraphController ``` :::info Event Names Must Match - Notice that the event name used in `PublishSubscriptionEvent()` is the same as the `EventName` property on the `[SubscriptionRoot]` attribute above. 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 above. This is how the subscription server knows what published event is tied to which subscription. This value is case-sensitive. ::: ### Subscription 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 own return type and use that as the event data source. It will automatically populate this field with the data from `PublishSubscriptionEvent()`; this argument is not exposed in the object graph. +In the example above, the data sent with `PublishSubscriptionEvent()` is the same as the first input parameter, called `eventData`, on the subscription method, which is the same as the return type of the subscription method. By default, GraphQL will look for a parameter with the same data type as the method's own return type and use that as the event data source. It will automatically populate this field with the data from `PublishSubscriptionEvent()`; 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. @@ -128,6 +131,7 @@ public class SubscriptionController : GraphController { [SubscriptionRoot("onWidgetChanged", typeof(Widget), EventName = "WIDGET_CHANGED")] public IGraphActionResult OnWidgetChanged( + // highlight-next-line [SubscriptionSource] WidgetInternal eventData, string filter) { @@ -138,7 +142,7 @@ public class SubscriptionController : GraphController } ``` -Here the subscription expects that an event is published using a `WidgetInternal` data type that it will internally convert to a `Widget` and send to any subscribers. This can be useful if you wish to share internal objects between your mutations and subscriptions that you don't want publicly exposed. +Here the subscription expects that an event is published using a `WidgetInternal` that it will 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. :::info Event Data Objects Must Match The data object published with `PublishSubscriptionEvent()` must have the same type as the `[SubscriptionSource]` on the subscription field. @@ -146,15 +150,15 @@ Here the subscription expects that an event is published using a `WidgetInternal ### Summary -That's all there is for a basic subscription server setup. +That's all there is for a basic subscription server setup: -1. Add the package reference and update startup.cs -2. Declare a new subscription using `[Subscription]` or `[SubscriptionRoot]` -3. Publish an event from a mutation +1. Add the package reference and update your startup code. +2. Declare a new subscription on a controller using `[Subscription]` or `[SubscriptionRoot]`. +3. Publish an event (usually from a mutation). ### Apollo Client Example -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. +📌 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 @@ -166,20 +170,20 @@ Here is a complete list of the various "subscription specific" action results: * `OkAndComplete(data)` - Works just like `this.Ok()` but ends the subscription after the event is completed. The client is informed that no additional data will be sent and that the server is closing the subscription permanently. This, however; does not close the underlying websocket connection. :::danger Be Careful With Sensitive Data -Data published via `PublishSubscriptionEvent()` is sent, automatically, to all active subscriptions on the server. +All active subscriptions, from all connected users, have an opportunity to handle data published via `PublishSubscriptionEvent()`. -If there are some scenarios where an event payload should not be shared with some connected users, be sure to enforce that business logic in your subscription method and use `SkipSubscriptionEvent()` as necessary for a given data package. +If there are scenarios where an event payload should not be shared with a user, be sure to enforce that business logic in your subscription method and use `SkipSubscriptionEvent()` for a given payload. ::: ## Scaling Subscription Servers Using web sockets has a natural limitation in that any single server instance has a maximum number of socket connections that it can realistically handle before being overloaded. Additionally, all cloud providers impose an artifical limit for many of their pricing tiers. Once that limit is reached no additional clients can connect, even if the server has capacity. -Ok no problem, just scale horizontally, spin up additional server instances, add a load balancer and have the new requests open a web socket connection to these additional server instances, right? Not so fast. +Ok no problem, just scale horizontally, right? Spin up additional server instances, add a load balancer and have the new requests open a web socket connection to these additional server instances...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 the event occured. 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 instance where the event was raised will receive it. Clients connected to other server instances will never know the event occured. 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. +[This diagram](../assets/2022-10-subscription-server.pdf) shows a high level difference between the default, single server configuration and a custom scalable solution. ### Custom Event Publishing @@ -192,13 +196,14 @@ Whatever your technology of choice the first step is to create and register a cu ```csharp title="ISubscriptionEventPublisher.cs" public interface ISubscriptionEventPublisher { - Task PublishEvent(SubscriptionEvent eventData); + Task PublishEventAsync(SubscriptionEvent eventData); } ``` Register your publisher with the DI container BEFORE calling `.AddGraphQL()` and GraphQL ASP.NET will use your registered publisher instead of its default, internal publisher. ```csharp title="Register Your Event Publisher" +// highlight-next-line services.AddSingleton(); services.AddGraphQL() @@ -219,7 +224,7 @@ Once you rematerialize a `SubscriptionEvent` you need to let GraphQL know that i The router will take care of the details in figuring out which schema the event is destined for, which clients have active subscriptions etc. and forward it accordingly. -```csharp title="Cusomer Event Listener Service" +```csharp title="Example Custom Event Listener Service" public class MyListenerService : BackgroundService { private readonly ISubscriptionEventRouter _router; @@ -230,46 +235,53 @@ The router will take care of the details in figuring out which schema the event _router = router; } - protected override async Task ExecuteAsync(CancellationToken t) + protected override async Task ExecuteAsync(CancellationToken cancelToken) { while (_notStopped) { - SubscriptionEvent eventData = /* Fetch Next Event*/; + SubscriptionEvent eventData = /* fetch next event */; + // highlight-next-line _router.RaisePublishedEvent(eventData); } } } ``` +:::info +The above example is made using [`Background Services`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-7.0&tabs=visual-studio). A feature of the modern ASP.NET stack. +::: + ### Azure Service Bus Example -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. +📌 A functional example, including serialization and deserialization using the Azure Service Bus is available in the [demo projects](../reference/demo-projects) section. -> The demo project represents a functional starting point and lacks a lot of the error handling and resilency needs of a production environment. +:::note + The Azure Service Bus demo project represents a functional starting point and lacks a lot of the error handling and resilency needs of a production environment. +::: -## Subscription Server Configuration +## Partial Server Configuration 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 and communicate the correct web socket connections. +1. The subscription server components are registered to the DI container, the graphql execution pipeline is modified to support clients registering subscriptions and a middleware component is appended to the ASP.NET pipeline to intercept and communicate with web socket connections. 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). +Some applications may wish to split these operations in different server instances for managing 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 granular configuration options may be useful: -- `.AddSubscriptionServer()` :: Only configures the ASP.NET pipeline to intercept websockets and adds the subscription server components to the DI container. +- `.AddSubscriptionServer()` :: Only configures the ASP.NET pipeline to intercept websockets and adds the subscription server components to the DI container. Mutations found on this server instance will **NOT** be successful in publishing events. - `.AddSubscriptionPublishing()` :: Only configures the graphql execution pipeline to publish events. Subscription creation and websocket support is **NOT** enabled. ## Security & Query Authorization -Because subscriptions are long running and registered before any data is processed, the subscription server requires a [query authorization method](../reference/schema-configuration#authorization-options) of `PerRequest`. This allows the subscription query to be fully validated before its registered with the server. This authorization method is set globally at startup and will apply to queries and mutations as well. +Because subscriptions are long running, consume server resources and registered before any data is processed, the subscription server requires a [query authorization method](../reference/schema-configuration#authorization-options) of `PerRequest`. This allows the subscription to be fully validated before its registered with the server and ensure that any queries can actually be processed when needed. This authorization method is set at startup and will apply to queries and mutations as well. It is because of this that it may be useful to split your mutations and subscriptions into different server instances for larger applications, as mentioned above, depending on your reliance on partial query resolution. -This is different than the default behavior when subscriptions are not enabled. Queries and mutations, by default, will follow a `PerField` method allowing for partial query resolutions. +This is different than the default behavior when subscriptions are not enabled. Queries and mutations, by default, will follow a `PerField` method allowing for partial query resolutions. :::caution Adding Subscriptions to your server will force the use of `PerRequest` Authorization @@ -277,7 +289,7 @@ This is different than the default behavior when subscriptions are not enabled. ## Query Timeouts -By default GraphQL does not define a timeout for an executed query. The query will run as long as the underlying HTTP connection is open. This is true for subscriptions as well. Given that the websocket connection is never closed while the end user is connected, any query executed through the websocket will be allowed to run for an infinite amount of time which can have some unintended side effects and consume resources unecessarily. +By default, the library does not define a timeout for an executed query. The query will run as long as the underlying connection is open. This is true for subscriptions as well. Given that the websocket connection is never closed while the end user is connected, any query executed through the websocket will be allowed to run for an infinite amount of time which can have some unintended side effects and consume resources unecessarily. Optionally, you can define a query timeout for a given schema, which the subscription server will obey: @@ -303,7 +315,7 @@ Out of the box, the library supports subscriptions over websockets using `graphq If you wish to add support for your own websocket messaging protocol you need to implement two interfaces: -- `ISubscriptionClientProxy` wraps a client connection and performs all the necessary communications. +- `ISubscriptionClientProxy` wraps a client connection, processes events and performs all necessary communications. - `ISubscriptionClientProxyFactory` which is used create client proxy instances for your protocol. @@ -323,7 +335,7 @@ public interface ISubscriptionClientProxyFactory } ``` -Inject it into your DI container before calling AddGraphQL: +Inject the factory into your DI container before calling AddGraphQL: ```csharp title="Register Your Protocol Client Factory" // startup @@ -334,7 +346,7 @@ services.AddGraphQL() ``` :::caution Create a Singleton Factory - `ISubscriptionClientProxyFactory` is expected to be a singleton; it is only instantiated once when the server first comes online. The `ISubscriptionClientProxy`instances it creates should be unique per `IClientConnection` instance. + `ISubscriptionClientProxyFactory` is expected to be a singleton; it is instantiated once when the server first comes online. The `ISubscriptionClientProxy`instances it creates should be unique per `IClientConnection` instance. ::: 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. @@ -345,48 +357,51 @@ The complete details of implementing a custom graphql client proxy are beyond th ## 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 websocket received from .NET. This interface is internally 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. +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 websocket received from ASP.NET. This interface is internally 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 intricacies 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 -In a production app, its very possible that you may have lots of subscription events fired and communicated to a lot of connected clients in short succession. 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 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. +In a production app, its very possible that you may have lots of subscription events fired and communicated to a lot of connected clients in short succession. Its important to understand how the server will process those events and plan accordingly. -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. +When the router receives an event it looks to see which clients are subscribed to that event and queues up a work item for each one. If there are 5 clients registered to the single event, then 5 work items are queued. Internally, this work is processed asyncronously to a server-configured maximum. Once this maximum is reached, new work will only begin as other work finishes up. -By default, the max number of work items the router will deliver simultaniously is `500`. 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. +Each work item is, for the most part, a single query execution. Though, if a client registers to a subscription multiple times each registration is executed as its own query. With lots of events being delivered on a server saturated with clients, each potentially having multiple subscriptions, along with regular queries and mutations executing...limits must be imposed otherwise CPU utilization could unreasonably spike...and it may spike regardless in some use cases. -```csharp title="Set A Receiver Throttle" -// Startup +By default, the max number of work items the router will deliver simultaniously is `500`. This is a global, server-wide pool, shared amongst all registered schemas. You can controller 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 title="Set A Receiver Throttle During Startup" // Adjust the max concurrent communications value // BEFORE calling .AddGraphQL() -SubscriptionServerSettings.MaxConcurrentReceiverCount = 500; +// highlight-next-line +GraphQLSubscriptionServerSettings.MaxConcurrentReceiverCount = 500; services.AddGraphQL() .AddSubscriptions(); ``` +:::note +The max receiver count can easily be set in the 1000s. There is no magic bullet as to an appropriate value. It all depends on the number of events you are expecting, the number of subscribers and the workload of each event in your application. +::: + ### 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. +Think carefully about your production scenarios when you introduce subscriptions into your application. As mentioned above, for each subscription event raised, each 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 `3 subscriptions` open against ONE event, thats `600 individual queries` that must be executed to process the event completely. Even if you call `SkipSubscriptionEvent` to drop the event and no send data to a client, the query still must be executed to determine the subscriber is not interested in the data. 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 process. +If, for instance, you have `200 clients` connected to a single server, each with `3 subscriptions` against the same field, thats `600 individual queries` that must be executed to process a single event completely. Even if you call `SkipSubscriptionEvent` to drop the event and send no send data to a client, the query still must be executed to determine if the subscriber is not interested in the data. If you execute any database operations in your `[Subcription]` method, its going to be run 600 times. 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 process. Balancing the load can be difficult. Luckily there are some [throttling levers](/docs/reference/global-configuration#subscriptions) you can adjust. -:::info Know Your User Traffic - Raising subscription events can exponentially increase the load on each of your servers. Think carefully when you deploy subscriptions to your application. +:::danger Know Your User Traffic + Raising subscription events can exponentially increase the load on each of your servers if not planned correctly. Think carefully when you deploy subscriptions to your application. ::: ### Dispatch Queue Monitoring -Internally, whenever a subscription server instance receives an event, the router checks to see which of the currently connected clients need to process that event. The client/event combination is then put into a dispatch queue that is continually processed via a background service according to the throttling limits you've specified. If events are received faster than they can be dispatched they are queued, in memory, until resources are freed up. +Internally, whenever a subscription server instance receives an event, the router checks to see which of the currently connected clients need to process that event. The client/event combination is then put into a dispatch queue that is continually processed via an internal, background service according to the throttling limits you've specified. If events are received faster than they can be dispatched they are queued, in memory, until resources are freed up. -There is built in monitoring of this queue that will automatically [record a log event](../logging/subscription-events.md#subscription-event-dispatch-queue-alert) when a given threshold is reached. +There is a built-in monitoring of this queue that will automatically [record a log event](../logging/subscription-events.md#subscription-event-dispatch-queue-alert) when a given threshold is reached. #### Default Event Alert Threshold This event is recorded at a `Critical` level when the queue reaches `10,000 events`. This alert is then re-recorded once every 5 minutes if the @@ -419,13 +434,13 @@ thresholds.AddThreshold( 100000, TimeSpan.FromSeconds(15)); -// register the interface as a singleton +// register as a singleton +// highlight-next-line services.AddSingleton(alerts); // normal graphql configuration -services.AddGraphQL(); - - +services.AddGraphQL() + .AddSubscriptions(); ``` > Consider using the built in `SubscriptionClientDispatchQueueAlertSettings` object for a standard implementation of the required interface. diff --git a/docs/advanced/type-expressions.md b/docs/advanced/type-expressions.md index 850b8c4..5aa8d39 100644 --- a/docs/advanced/type-expressions.md +++ b/docs/advanced/type-expressions.md @@ -50,6 +50,7 @@ You can add more specificity to your fields by using the `TypeExpression` proper // Declare that a donut MUST be returned (null is invalid) // ---- // Final Schema Syntax: Donut! +// highlight-next-line [Query("donut", TypeExpression = "Type!")] public Donut RetrieveDonut(string id) {/*...*/} @@ -62,6 +63,7 @@ public Donut RetrieveDonut(string id) // invalid: null // ---- // Final Schema Syntax: [Donut]! +// highlight-next-line [Query("donut", TypeExpression = "[Type]!")] public IEnumerable RetrieveDonut(string id) {/*...*/} @@ -75,6 +77,7 @@ public IEnumerable RetrieveDonut(string id) // invalid: null // ---- // Final Schema Syntax: [Donut!]! +// highlight-next-line [Query("donut", TypeExpression = "[Type!]!")] public IEnumerable RetrieveDonut(string id) {/*...*/} @@ -89,6 +92,7 @@ Note that the library will accept your type string even if it would be impossibl ```csharp title="Data and Type Expression Mismatch" // QUERY EXECUTION ERROR // GraphQL will attempt to process a Donut as an IEnumerable and will fail +// highlight-next-line [Query("donut", TypeExpression ="[Type]")] public Donut RetrieveDonut(string id) {/*...*/} @@ -109,6 +113,7 @@ Similar to fields, you can use the `TypeExpression` property on `[FromGraphQL]` // ----------------- // Final Type Expression of the 'id' arg: String! [Query] +// highlight-next-line public Donut RetrieveDonut([FromGraphQL(TypeExpression = "Type!")] string id) {/*...*/} ``` \ No newline at end of file diff --git a/docs/assets/create-new-web-api-project.png b/docs/assets/create-new-web-api-project.png new file mode 100644 index 0000000..7e90217 Binary files /dev/null and b/docs/assets/create-new-web-api-project.png differ diff --git a/docs/assets/overview-sample-query-results.png b/docs/assets/overview-sample-query-results.png new file mode 100644 index 0000000..ace8a97 Binary files /dev/null and b/docs/assets/overview-sample-query-results.png differ diff --git a/docs/controllers/actions.md b/docs/controllers/actions.md index 32fbc44..7215495 100644 --- a/docs/controllers/actions.md +++ b/docs/controllers/actions.md @@ -5,14 +5,16 @@ sidebar_label: Actions sidebar_position: 0 --- + ## What is an Action? + :::info - An action is method on a controller that can fulfill a request for a field + An action is a method on a controller, marked as being a query or mutation field, that is part of your graph schema. ::: -Controllers and actions are the bread and butter of GraphQL ASP.NET. Every top level field for any mutation or query **must** be an action method and, like with MVC, serves as an entry point into your business logic. +Controllers and actions are the bread and butter of GraphQL ASP.NET. Just like with Web API, they serve as an entry point into your business logic. -In this graphql query we have two top level fields: `hero` and `droid`. We declare action methods for each to handle the data request. +In this graphql query we have two top level query fields: `hero` and `droid`. We declare action methods for each to handle the data request. ```graphql title="Sample Query" query { @@ -29,9 +31,6 @@ query { ``` ```csharp title="Controllers.cs" -using GraphQL.AspNet.Controllers; -using GraphQL.AspNet.Attributes; - // HeroController.cs public class HeroController : GraphController { @@ -53,9 +52,7 @@ public class DroidController : GraphController } ``` - - -In the above example, it makes sense these these methods would exist on different controllers, `HeroController` and `DroidController`. Unlike with a REST API request, which will usually invokes one action method and returns the data generated, GraphQL will automatically invoke every action method requested and aggregates the results. Each action is executed asynchronously and in isolation. If one fails, the other may not and the results of both the errors and the data retrieved would be returned. +In the above example, it makes sense these these methods would exist on different controllers, `HeroController` and `DroidController`. However, unlike with a REST API request, which will usually invoke one action method and returns the data generated, GraphQL will invoke every action method requested and aggregates the results. If one action fails, the other may not and the results of both the errors and the data retrieved would be returned. The data returned by your action methods then return their requested child fields, those data items to their children and so on. In many cases this is just a selection of the appropriate properties on a model object, but more complex scenarios involving child objects, [type extensions](./type-extensions) or directly executing POCO methods can also occur. @@ -66,15 +63,13 @@ Since mutations may have a shared state or could otherwise produce race conditio To declare an action you first declare a controller that inherits from `GraphQL.AspNet.Controllers.GraphController`, then create a method with the the appropriate attribute to indicate it as a query or mutation method. -An Action Method: - -- **May** be synchronous or asynchronous -- **Must** declare an [operation type](./actions#declaring-an-operation-type) or a [type extension](../controllers/type-extensions) attribute -- **Must** declare a return type - - `void` is not allowed. - - Asynchronous methods **must** return `Task`. -- **Must not** return `System.Object`. -- **Must not** declare, as an input parameter, an object that implements any variation of `IDictionary`. +**An Action Method:** + + ✅ **Must** declare an [operation type](./actions#declaring-an-operation-type) or a [type extension](../controllers/type-extensions) attribute.
+ ✅ **Must** declare a return type.
+ ✅ **May** be synchronous or asynchronous.
+ 🧨 **Must not** return `System.Object`.
+ 🧨 **Must not** declare, as an input parameter, an object that implements any variation of `IDictionary`.
See below for more detail of the [input](./actions#method-parameters) and [return](./actions#returning-data) parameter restrictions. @@ -82,53 +77,178 @@ See below for more detail of the [input](./actions#method-parameters) and [retur Attributes `[Query]`, `[QueryRoot]`, `[Mutation]` and `[MutationRoot]` are used to declare your action methods. Usage of the mutation and query attributes are exactly the same, they differ only in which of the root graph types to place a field reference. Lets look at the most common ways to use them: -- `[Query]`, `[Mutation]` +
- - When used alone, without any parameters, the runtime will register your method with a field name that is the same as the method name. +💻 `[Query]`, `[Mutation]` -- `[Query("manager")]`,`[Mutation("manager")]` +When used alone, without any parameters, the field name in the schema is the same as the method name. - - You can supply the name of the field you want to use in the object graph allowing for different naming schemes in your C# code vs. your object graph (e.g. `RetrieveManager` vs. `manager` ). - - The meta name `"[action]"` can also be used (e.g. `[Query[("Another_[action]")]`) and will be replaced with the method name at runtime. +```csharp title="Using [Query] & [Mutation]" +public class BakeryController : GraphController +{ + // highlight-next-line + [Query] + public Donut FindDonut(int id) + {/* ... */ } + + // highlight-next-line + [Mutation] + public CakeModel UpdateCake(CakeModel cake) + {/* ... */ } +} +``` -- `[Query("manager", typeof(Manager))]`, `[Mutation("manager", typeof(Manager))]` +```graphql title="Sample Query" +query { + bakery { + findDonut(id: 5){ + name + flavor + } + } +} - - Sometimes, especially when you return action results, you may need to explicitly declare what data type you are returning from the method. This is because returning `IGraphActionResult` obfuscates the results from the templating engine and it won't be able to infer the underlying type expression of the field. +mutation { + bakery { + updateCake(cake: {id: 5, name: "Birthday Cake"}){ + id + name + } + } +} +``` -- `[QueryRoot]`, `[MutationRoot]` - - These are special overloads to `[Query]` and `[Mutation]`. They are declared in the same way but instruct GraphQL to ignore any inherited field fragments from the controller. +
+
-If you'll recall, Web API uses `[Route("somePathSegment")]` declared on a controller to nest all the REST end points under that url piece. The same holds true here. Graph controllers can declare `[GraphRoute("someFieldName")]` under which all the controller actions will be nested as child fields. This is the default behavior even if you don't declare a custom name (your controller name is used). Using the `QueryRoot` and `MutationRoot` attributes negates this and appends the action directly to the root graph type. +💻 `[Query("donut")]`,`[Mutation("alterCake")]` -A complete explanation of the constructors for these attributes is available in the [attributes reference](../reference/attributes) and a detailed explanation of the nesting rules is available under the [field paths](./field-paths) section. +You can supply the name of the field you want to use in the schema allowing for different naming schemes in your C# code vs. your object graph. -## Returning Data +```csharp title="Using [Query] & [Mutation]" +public class BakeryController : GraphController +{ + // highlight-next-line + [Query("donut")] + public Donut FindDonut(int id) + {/* ... */ } + + // highlight-next-line + [Mutation("alterCake")] + public CakeModel UpdateCake(CakeModel cake) + {/* ... */ } +} +``` -An object graph is built from a hierarchy of data types and the fields they expose. GraphQL creates this by first looking your action methods for the objects they return. It then inspects the properties/methods on those objects for other objects, then the properties of those child objects and so on. In GraphQL, there are no unknown or variable fields. With REST, you can query a URI to an unknown resource and the server will happily return a 404, telling you the URI doesn't point to anything. +```graphql title="Sample Query" +query { + bakery { + donut(id: 5){ + name + flavor + } + } +} + +mutation { + bakery { + alterCake(cake: {id: 5, name: "Birthday Cake"}) { + id + name + } + } +} +``` + +
+
+ +💻 `[Query("donut", typeof(Donut))]`, `[Mutation("donut", typeof(CakeModel))]` -```javascript title="REST" -Request: GET https://mysite.com/fake/resource/86 -Response: 404 /fake/resource/86 not found + Sometimes, especially when you return action results, you may need to explicitly declare what data type you are returning from the method. This is because returning `IGraphActionResult` obfuscates the results from the templating engine and it won't be able to infer the underlying type expression of the field. + + +```csharp title="Using [Query] & [Mutation]" +public class BakeryController : GraphController +{ + [Query("donut")] + public Donut FindDonut(int id) + {/* ... */ } + + // highlight-next-line + [Mutation("alterCake",typeof(CakeModel))] + public async Task UpdateCake(CakeModel cake) + { + await _service.UpdateCake(cake); + return this.Ok(cake); + } +} ``` -In GraphQL, however; the runtime will reject a query outright siting non-existent field names. +
+
+ +💻 `[QueryRoot]`, `[MutationRoot]` -```graphql title="GraphQL" +These are special overloads to `[Query]` and `[Mutation]`. They are declared in the same way but instruct GraphQL to ignore any inherited field fragments from the controller. + +```csharp title="Using [QueryRoot]" +public class BakeryController : GraphController +{ + // highlight-next-line + [QueryRoot("donut")] + public Donut FindDonut(int id) + {/* ... */ } +} +``` + +```graphql title="Sample Query" +# Notice the bakery field is gone! query { - fake { - resource(id: 86) { - name - } + donut(id: 5){ + name + flavor } +} +``` + +
+
+ +💻 `[GraphRoute]` + +If you'll recall, Web API uses `[Route("somePathSegment")]` declared on a controller to nest all the REST end points under that url piece. The same holds true here. Graph controllers can declare `[GraphRoute("someFieldName")]` under which all the controller actions will be nested as child fields. This is the default behavior even if you don't declare a custom name (your controller name is used). Using the `QueryRoot` and `MutationRoot` attributes negates this and appends the action directly to the root graph type. + + +```csharp title="Using [GraphRoute]" +// highlight-next-line +[GraphRoute("BakedGoods")] +public class BakeryController : GraphController +{ + [Query("donut")] + public Donut FindDonut(int id) + {/* ... */ } } +``` -# Error -# Invalid Query, no such field as "fake" on the type "query" +```graphql title="Sample Query" +query { + bakedGoods { + donut(id: 5){ + name + flavor + } + } +} ``` -:::info - All action methods **MUST** return an object; be that a single item or an array of objects. -::: +A complete explanation of the constructors for these attributes is available in the [attributes reference](../reference/attributes) and a detailed explanation of the nesting rules is available under the [field paths](./field-paths) section. + +## Returning Data + +GraphQL creates your schema by first looking your controller action methods for the objects they return. It then inspects the properties/methods on those objects for other objects, then the properties of those child objects and so on. In GraphQL, there are no unknown or variable fields. For the library to determine your schema it MUST know what each action method returns. + +Unlike rest, the data you return is restricted to formats acceptable by graphql. ### Working with Dictionaries @@ -147,7 +267,9 @@ Rules concerning GraphQL's two meta graph types, [LIST and NON_NULL](../types/li Returning an [interface graph type](../types/interfaces) is a great way to deliver heterogeneous data results, especially in search operations. One got'cha is that the runtime must know the possible concrete object types that implement that interface in case a query uses a fragment with a type specification. That is to say that if we return `IPastry`, we must let GraphQL know that `Cake` and `Donut` exist and should be a part of our schema. Just like with C#, interfaces in graphql contain no logic. If i return an `IPastry`, GraphQL still needs to know if the actual object is a `Cake` or a `Donut` and invoke the correct resolver for any child fields. :::info -When your action method returns an interface you must let GraphQL know of the OBJECT types that implement that interface in some other way. If your schema contains `IPastry`, it must also contain `Cake` and `Donut`. +When your action method returns an interface you must declare OBJECT types that implement that interface in some other way. + +e.g. If your schema contains `IPastry`, it must also contain `Cake` and `Donut`. ::: Take this example: @@ -155,6 +277,7 @@ Take this example: public class BakeryController : GraphController { [QueryRoot] + // highlight-next-line public IPastry SearchPastries(string name) {/* ... */} } @@ -181,20 +304,25 @@ No where in our code have we told GraphQL about `Cake` or `Donut`. When it goes There are a number of ways to indicate these required relationships in your code in order to generate your schema correctly. -#### Add OBJECT Types Directly to the Action Method +
+ + 📃 **Add OBJECT Types Directly to the Action Method** -If you have just two or three possible types, add them directly to the query attribute. You can safely add `typeof(Cake)` across multiple methods as needed, it will only be included in the schema once. +If you have just two or three possible types, add them directly to the query attribute. You can safely add your types across multiple methods as needed, it will only be included in the schema once. ```csharp public class BakeryController : GraphController { + // highlight-next-line [QueryRoot(typeof(Cake), typeof(Donut))] public IPastry SearchPastries(string name) {/* ... */} } ``` -#### Using the PossibleTypes attribute +
+ +📜 **Using the PossibleTypes attribute** The `[Query]` attribute can get a bit hard to read with a ton of data in it (especially with [Unions](../types/unions)). Use the `[PossibleTypes]` attribute to help with readability. @@ -202,13 +330,16 @@ The `[Query]` attribute can get a bit hard to read with a ton of data in it (esp public class BakeryController : GraphController { [QueryRoot] + // highlight-next-line [PossibleTypes(typeof(Cake), typeof(Donut), typeof(Scone), typeof(Croissant))] public IPastry SearchPastries(string name) {/* ... */} } ``` -#### Declare Types at Startup +
+ +🧮 **Declare Types at Startup** The [schema configuration](../reference/schema-configuration) contains a host of options for auto-loading graph types. Here we've added our 100s and 1000s of types of pastries at our bakery to a shared assembly, obtained a reference to it through one of the types it contains, then added the whole assembly to our schema. GraphQL will automatically scan the assembly and ingest all the graph types mentioned in any controllers it finds as well as any objects marked with the `[GraphType]` attribute. @@ -217,28 +348,36 @@ The [schema configuration](../reference/schema-configuration) contains a host of Assembly pastryAssembly = Assembly.GetAssembly(typeof(Cake)); services.AddGraphQL(options => - { - options.AddAssembly(pastryAssembly); - }); +{ + // highlight-next-line + options.AddAssembly(pastryAssembly); +}); ``` +
+ +⚠️ **A Note On Type Ingestion** + + You might be wondering, "if I just define `Cake` and `Donut` in my application, why can't GraphQL just include them like it does the controller?". -It certainly can, but there are risks to arbitrarily grabbing class references not exposed via a `GraphController`. With introspection queries, all of those classes and their method/property names could be exposed and pose a security risk. Imagine if a enum named `EmployeeDiscountCodes` was accidentally added to your graph. +It certainly can, but there are risks to arbitrarily grabbing class references not exposed on a schema. With introspection queries, all of those classes and their method/property names could be exposed and pose a security risk. It might not be able to query the data, but imagine if a enum named `EmployeeDiscountCodes` was accidentally added to your graph. All the values of that enum would be publically exposed via introspection. To combat this GraphQL will only ingest types that are: -- Referenced in a `GraphController` -- Attributed with at least once instance of `[GraphType]` or `[GraphField]`somewhere within the type definition -- Added at startup during `.AddGraphQL()`. +- Referenced in a `GraphController` action method **OR** +- Attributed with at least once instance of a `[GraphType]` or `[GraphField]` attribute somewhere within the class **OR** +- Added explicitly at startup during `.AddGraphQL()`. This behavior is controlled with your schema's declaration configuration to make it more or less restrictive based on your needs. Ultimately you are in control of how aggressive or restrictive GraphQL should be; even going so far as declaring that every type be declared with `[GraphType]` and every field with `[GraphField]` lest it be ignored completely. The amount of automatic vs. manual wire up will vary from use case to use case but you should be able to achieve the result you desire. ### Graph Action Results -Action Results provide a clean way to standardize your responses to different conditions across your application. In a Web API controller, if you've ever used `this.OK()` or `this.NotFound()` you've used the concept of an action result before. The full list can be found in the [action results](../advanced/graph-action-results) reference section. +Action Results provide a clean way to standardize your responses to different conditions across your application. In a Web API controller, if you've ever used `this.OK()` or `this.NotFound()` you've used the concept of an action result before. -Using action results can make your code a lot more readable and provide helpful, customizable messaging to the requestor. +Using action results can make your code a lot more readable and provide helpful, customizable messaging to the requestor. + +For Example, using `this.Error()` injects a custom error message into the response providing some additional information other than just a null result. ```csharp title="BakeryController.cs" // BakeryController.cs @@ -249,18 +388,22 @@ public class BakeryController : GraphController { if(name == null || name.Length < 3) { + // highlight-next-line return this.Error(GraphMessageSeverity.Warning, "At least 3 characters is required"); } else { - var results = await _service.SearchPastries(name); + var results = await _service.SearchPastries(name); return this.Ok(results); } } } ``` -Using `this.Error()` injects a custom error message into the response providing some additional information other than just a null result. Create a class that implements `IGraphActionResult` and create your own. +> The full list of graph action results can be found in the [reference section](../advanced/graph-action-results) . + + +Create a class that implements `IGraphActionResult` and create your own. ```csharp title="IGraphActionResult.cs" public interface IGraphActionResult @@ -269,20 +412,25 @@ public interface IGraphActionResult } ``` -`IGraphActionResult`has one method. It accepts the raw resolution context (either a field resolution context or a directive resolution context) that you can manipulate as needed. Combine this with any data you supply to your action result when you instantiate it and you have the ability to generate any response type with any data value or any number and type of error messages etc. Take a look at the source code for built in [action results](https://github.com/graphql-aspnet/graphql-aspnet/tree/master/src/graphql-aspnet/Controllers/ActionResults) for some examples. +`IGraphActionResult` has one method. It accepts the raw resolution context (either a `FieldResolutionContext` or a `DirectiveResolutionContext`) that you can manipulate as needed. Combine this with any data you supply to your action result when you instantiate it and you have the ability to generate any response with any data value or any number and type of error messages etc. Take a look at the source code for built in [graph action results](https://github.com/graphql-aspnet/graphql-aspnet/tree/master/src/graphql-aspnet/Controllers/ActionResults) for some more detailed examples. + +:::caution Its Not REST +Action results for graph fields are not the same as REST action results. For Example, `BadRequest()` does not return an HTTP status 400 for the request. An errored field is usually just one of many in the query and graphql supports partial query resolution. We use the `errors` collection on a graphql response to provide details on what happened with any given field. The overall query will almost always return an HTTP status 200. +::: ## Method Parameters -GraphQL will inspect your parameters and generate the appropriate [`SCALAR`](../types/scalars), [`ENUM`](../types/enums) and [`INPUT_OBJECT`](../types/input-objects) graph types to represent them. +GraphQL will inspect your method parameters and add the appropriate [`SCALAR`](../types/scalars), [`ENUM`](../types/enums) and [`INPUT_OBJECT`](../types/input-objects) graph types to your schema automatically. ### Naming your Input Arguments -By default, GraphQL will name a field's arguments the same as the parameter names in your method. Sometimes you'll want to override this, like when needing to use a C# keyword as an argument name. Use the `[FromGraphQL]` attribute on the parameter to accomplish this. +By default, GraphQL will name a field' arguments the same as the parameter names in your method. Sometimes you'll want to override this, like when needing to use a C# keyword as an argument name. Use the `[FromGraphQL]` attribute on the parameter to accomplish this. ```csharp title="Overriding a Default Argument Name" public class BakeryController : GraphController { - [QueryRoot] + [QueryRoot] + // highlight-next-line public IEnumerable SearchDonuts([FromGraphQL("name")] string searchText) {/* ... */} } @@ -301,18 +449,15 @@ query { } ``` -Like with action methods, the meta name `"[parameter]"` can be used to represent the C# parameter name and at runtime will be dynamically injected. - -### Default Parameter Values - -In GraphQL, not all input values are required. Those that declare a default value are optional in a submitted query: - +### Default Argument Values +In GraphQL, not all field arguments are required. Add a default value to your method parameters to mark them as optional: -```csharp title="BakeryController.cs" +```csharp title="Using an Optional Field Argument" public class BakeryController : GraphController { - [QueryRoot] + [QueryRoot] + // highlight-next-line public Donut SearchDonuts(string name = "*") {/* ... */} } @@ -327,7 +472,7 @@ query { } } -# Use default value for name (i.e. "*") +# The default value for name will be used (e.g. "*") query { searchDonuts { id @@ -336,12 +481,18 @@ query { } ``` -Note that there is a difference between "nullable" and "not required" for input arguments. If we have a nullable int as an input parameter, without a default value we still have to pass it to the field, even if we pass it as null just like if we were to invoke the method from our C# code. +
+ +⚠️ **Nullable vs. Not Required** + +Note that there is a difference between "nullable" and "not required" for field arguments. If we have a nullable int as an input parameter, without a default value we still have to pass it to the field, even if we pass it as `null`; just like if we were to invoke the method from our C# code. ```csharp title="NumberController.cs" public class NumberController : GraphController { - [QueryRoot] + // "seed" is still required, but you can supply null + [QueryRoot] + // highlight-next-line public int CreateRandomInt(int? seed) {/* ... */} } @@ -354,7 +505,9 @@ query { createRandomInt(seed: null) } +## *** ## ERROR, argument not supplied +## *** query { createRandomInt } @@ -362,11 +515,11 @@ query { By also defining a default value we can achieve the flexibility we are looking for. - ```csharp title="NumberController.cs" public class NumberController : GraphController -{ +{ [QueryRoot] + // highlight-next-line public int CreateRandomInt(int? seed = null) {/* ... */} } @@ -391,7 +544,11 @@ query { ### Working With Lists -When constructing a set of items as an argument to an action method, GraphQL will instantiate a `List` and fill it with the appropriate data, be that another list, another input object, a scalar etc. While you can declare an array (e.g. `Donut[]`, `int[]` etc.) as your list structure for an input argument, graphql has to rebuild its internal representation as an array (or nested arrays) to meet the requirements of your method. In some cases, especially with nested lists, this results in an `O(N)` increase in processing time. It is recommended to use `IEnumerable` or `IList` to avoid this performance bottleneck when sending lots of items as input data. +When constructing a set of items as an argument to an action method, GraphQL will instantiate a `List` internally and fill it with the appropriate data; be that another list, another input object, a scalar etc. While you can declare an array (e.g. `Donut[]`, `int[]` etc.) as your list structure for an input argument, graphql has to rebuild its internal representation as an array (or nested arrays) to meet the requirements of your method. In some cases, especially with nested lists, this results in an `O(N)` increase in processing time. + +:::tip +Use `IEnumerable` or `IList` as your argument types to avoid a performance bottleneck when sending lots of items as input data. +::: This example shows various ways of accepting collections of data as inputs to controller actions. @@ -416,7 +573,7 @@ public class BakeryController : GraphController public bool DonutsAsAnArray(Donut[] donuts) {/*....*/} - // This is a valid nested list + // This is a valid nested list believe it or not // schema syntax: [[[Donut]]] [Mutation("mixedDonuts")] public bool MixedDonuts(List> donuts) @@ -455,9 +612,9 @@ query { } ``` -At runtime, GraphQL will try to validate every argument on every field passed on a query against the schema. No where have we declared an argument `filled` to be a boolean or `name` to be a string. +At runtime, GraphQL will try to validate every argument on every field passed on a query. No where have we declared an argument `filled` to be a boolean or `name` to be a string. -One might think, well it should be passed as an object reference to the dictionary parameter: +Well, lets just pass it as an input object to a declared argument, right? ```graphql title="Invalid Input Object" # ERROR, Unknown fields on searchParams @@ -471,7 +628,7 @@ query { But this is also not allowed. All we've done is pushed the problem down one level. No where on our `IDictionary` type is there a `Name` property declared as a string or a `Filled` property declared as a boolean. Since GraphQL can't fully validate the query against the schema before executing it, it's rejected. -Instead declare the search object with the parameters you need and use it as the input: +Instead declare a search object with the parameters you need and use it as the input: ```csharp title="BakeryController.cs" public class DonutSearchParams @@ -500,13 +657,14 @@ query { ## Cancellation Tokens -As with REST based ASP.NET action methods, your graph controller action methods can accept an optional `CancellationToken`. This is useful when doing some long running activities such as IO, database queries, API orchestration etc. To make use of a cancellation token simply add it as a parameter to your method. GraphQL will automatically wire up the token for you: +As with REST based action methods, your graph controller action methods can accept an optional `CancellationToken`. This is useful when doing some long running activities such as IO, database queries, API orchestration etc. To make use of a cancellation token simply add it as a parameter to your method. GraphQL will automatically capture the token, wire it up for you and hide it from your schema. ```csharp title="BakeryController.cs | Adding a CancellationToken" public class BakeryController : GraphController { // Add a CancellationToken to your controller method [QueryRoot(typeof(IEnumerable))] + // highlight-next-line public async Task SearchDonuts(string name, CancellationToken cancelToken) {/* ... */} } @@ -518,7 +676,7 @@ public class BakeryController : GraphController ### Defining a Query Timeout -By default GraphQL does not define a timeout for an executed query. The query will run as long as the underlying HTTP connection is open. In fact, the `CancellationToken` passed to your action methods is the same Cancellation Token offered on the HttpContext when it receives the initial request. +By default, the library does not define a timeout for an executed query. The query will run as long as the underlying HTTP connection is open. In fact, the `CancellationToken` passed to your action methods is the same Cancellation Token offered on the HttpContext when it receives the initial request. Optionally, you can define a query timeout for a given schema: @@ -526,11 +684,12 @@ Optionally, you can define a query timeout for a given schema: services.AddGraphQL(o => { // define a 2 minute timeout for every query. + // highlight-next-line o.ExecutionOptions.QueryTimeout = TimeSpan.FromMinutes(2); }) ``` -When a timeout is defined, the token passed to your action methods is a combined token representing the HttpContext as well as the timeout operation. That is to say the token will indicate a cancellation if the alloted query time expires or the http connection is closed, which ever comes first. When the timeout expires the caller will receive a response indicating the timeout. However, if the its the HTTP connection that is closed, the operation is simply halted and no result is produced. +When a timeout is defined, the token passed to your action methods is a combined token representing the HttpContext as well as the timeout operation. That is to say the token will indicate a cancellation if the allotted query time expires or the http connection is closed, which ever comes first. When the timeout expires the caller will receive a response indicating the timeout. However, if the its the HTTP connection that is closed, the operation is simply halted and no result is produced. :::danger Timeouts and Subscriptions The same rules for cancellation tokens apply to subscriptions as well. Since the websocket connection is a long running operation it will never be closed until the connection is closed. To prevent some processes from spinning out of control its a good idea to define a query timeout when implementing a subscription server. This way, even though the connection remains open the query will terminate and release resources if something goes awry. diff --git a/docs/controllers/authorization.md b/docs/controllers/authorization.md index fe73401..5d3839e 100644 --- a/docs/controllers/authorization.md +++ b/docs/controllers/authorization.md @@ -7,11 +7,14 @@ sidebar_position: 3 ## Quick Examples -If you've wired up ASP.NET authorization before, you'll likely familiar with the `[Authorize]` attribute and how its used to enforce security. GraphQL ASP.NET works the same way. +If you've wired up ASP.NET authorization before, you'll likely familiar with the `[Authorize]` attribute and how its used to enforce security. + +GraphQL ASP.NET works the same way. ```csharp title="General Authorization Check" public class BakeryController : GraphController { + // highlight-next-line [Authorize] [MutationRoot("orderDonuts", typeof(CompletedDonutOrder))] public async Task OrderDonuts(DonutOrderModel order) @@ -21,6 +24,7 @@ public class BakeryController : GraphController ```csharp title="Restrict by Policy" public class BakeryController : GraphController { + // highlight-next-line [Authorize(Policy = "CustomerLoyaltyProgram")] [MutationRoot("orderDonuts", typeof(CompletedDonutOrder))] public async Task OrderDonuts(DonutOrderModel order) @@ -30,6 +34,7 @@ public class BakeryController : GraphController ```csharp title="Restrict by Role" public class BakeryController : GraphController { + // highlight-next-line [Authorize(Roles = "Admin, Employee")] [MutationRoot("purchaseDough")] public async Task PurchaseDough(int kilosOfDough) @@ -39,9 +44,14 @@ public class BakeryController : GraphController ```csharp title="Multiple Authorization Requirements" // The library supports nested policy and role checks at Controller and Action levels. +// highlight-next-line [Authorize(Policy = "CurrentCustomer")] public class BakeryController : GraphController { + // The user would have to pass the CurrentCustomer policy + // and the LoyaltyProgram policy to access the `orderDonuts` field + + // highlight-next-line [Authorize(Policy = "LoyaltyProgram")] [MutationRoot("orderDonuts", typeof(CompletedDonutOrder))] public async Task OrderDonuts(DonutOrderModel order) @@ -59,6 +69,7 @@ public class BakeryController : GraphController {/*...*/} // No Authorization checks on RetrieveDonutList + // highlight-next-line [AllowAnonymous] [Mutation("donutList")] public async Task> RetrieveDonutList() @@ -68,21 +79,16 @@ 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 [Schema Item Authorization Pipeline](https://github.com/graphql-aspnet/graphql-aspnet/tree/master/src/graphql-aspnet/Middleware/SchemaItemSecurity) 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()` at startup. 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? -![Authorization Flow](../assets/authorization-flow.png) - -_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). 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 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. -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: +Execution directives and field resolutions are passed through the libraries internal [pipeline](../reference/how-it-works#middleware-pipelines) where securty is enforced as a series of middleware components before the respective resolvers are invoked. Should a requestor not be authorized for a given schema item they are informed via an error message and denied access to the item. ## Field Authorizations @@ -97,11 +103,65 @@ Since this authorization occurs "per field" and not "per controller action" its ### 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 `SchemaItemAuthorizationCompleted` 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. Suppose there was a query where the user requests the `allDonuts` field but is denied access: -## Execution Directives Authorizations +```graphql + { + donut(id: 5) { + name + } + allDonuts { + name + } -Execution directives are applied to the _query document_ before a query plan is created, but 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 intended to ask for. + } + +``` + +The result might look like this: + +```json title="Denied Field Access" +{ + "errors": [ + // highlight-start + { + "message": "Access Denied to field [query]/allDonuts", + "locations": [ + { + "line": 7, + "column": 3 + } + ], + "path": [ + "allDonuts" + ], + "extensions": { + "code": "ACCESS_DENIED", + "timestamp": "2022-12-22T22:22:25.017-07:00", + "severity": "CRITICAL" + } + } + // highlight-end + ], + "data": { + "donut": { + "name": "Super Mega Donut", + }, + // highlight-next-line + "allDonuts": null + } +} +``` + +:::tip + To view more details authorization failure 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 `SchemaItemAuthorizationCompleted` log event at a `Warning` level when a security check fails. +::: + +## Authorization on Execution Directives + +Execution directives are applied to the _query document_, before a query plan is created to fulfill the request. However, it is the query plan that determines which field resolvers should be called. As a result, execution directives have the potential to alter the document structure and change how a query plan might be structured. Because of this, not executing a query directive has the potential to cause a the expected query to be different than what the requestor intended. Therefore, if an execution directive fails authorization the query is rejected and not executed. The caller 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. @@ -122,6 +182,10 @@ services.AddGraphQL(schemaOptions => }); ``` -:::info -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 failed and no data will be resolved. +## Performance Considerations + +Authorization is not free. Rhere is a minor, but real, performance cost to inspecting and evaluating policies on a field. This true regardless of yor choice of `PerField` or `PerRequest` authorization. Every secure field still needs to be evaluated, whether they are done up front or as the query progresses. In a REST query, you generally only secure your top-level controller methods, consider doing the same with your GraphQL queries. + +:::tip +Centralize your authorization checks to your controller methods. There is usually no need to apply `[Authorize]` attributes to each and every method and property across your entire schema. ::: \ No newline at end of file diff --git a/docs/controllers/batch-operations.md b/docs/controllers/batch-operations.md index 33661eb..0b96beb 100644 --- a/docs/controllers/batch-operations.md +++ b/docs/controllers/batch-operations.md @@ -49,15 +49,13 @@ query { } ``` -Well that was easy, right? Not so fast. The `bakeries` field returns a `List` but the `RetrieveCakeOrders` method takes in a single `Bakery`. GraphQL will, **for each bakery retrieved**, execute the `orders` field to retrieve its orders. If `bakeries` retrieved 50 stores in the south west region, graphql will execute `RetrieveCakeOrders` 50 times, which will execute 50 database queries. +Well that was easy, right? Not so fast! -This is the N+1 problem. `1 query` for the bakeries + `N queries` for the cake orders, where N is the number of bakeries first retrieved. - -If we could _batch_ the cake orders request and fetch all the orders for all the bakeries at once, then assign the `Cake Orders` back to their respective bakeries, we'd be a lot better off. No matter the number of bakeries retrieved, we'd execute 2 queries; 1 for `bakeries` and 1 for `orders`. + The `bakeries` field returns a `List` but the `RetrieveCakeOrders` method takes in a single `Bakery`. GraphQL will, **for each bakery retrieved**, execute the `orders` field to retrieve its orders. If `bakeries` retrieved 50 stores in the south west region, graphql will execute `RetrieveCakeOrders` 50 times, which will execute 50 database queries. -## Data Loaders +This is the N+1 problem. `1 query` for the bakeries + `N queries` for the cake orders, where N is the number of bakeries first retrieved. -You'll often hear the term `Data Loaders` when reading about GraphQL implementations. Methods that load the child data being requested as a single operation before assigning to each of the parents. There is no difference with GraphQL ASP.NET. You still have to write that method. But with the ability to capture action method parameters and clever use of an `IGraphActionResult` we can combine the data load phase with the assignment phase into a single batch operation, at least on the surface. The aim is to make it easy to read and easier to write. +If only we could batch the cake orders request and fetch all the orders for all the bakeries at once, then assign the `Cake Orders` back to their respective bakeries, we'd be a lot better off. No matter the number of bakeries retrieved, we'd execute 2 queries; 1 for `bakeries` and 1 for `orders`.This is where batch extensions come in to play. ## \[BatchTypeExtension\] Attribute @@ -70,6 +68,7 @@ public class BakedGoodsCompanyController : GraphController public async Task> RetrieveBakeries(Region region){/*...*/} // declare the batch operation as an extension + // highlight-next-line [BatchTypeExtension(typeof(Bakery), "orders", typeof(List))] public async Task RetrieveCakeOrders( IEnumerable bakeries, @@ -97,6 +96,10 @@ The contents of your extension method is going to vary widely from use case to u GraphQL works behind the scenes to pull together the items generated from the parent field and passes them to your batch method. +## Data Loaders + +You'll often hear the term `Data Loaders` when reading about GraphQL implementations. Methods that load the child data being requested as a single operation before assigning to each of the parents. There is no difference with GraphQL ASP.NET. You still have to write that method. But with the ability to capture action method parameters and clever use of an `IGraphActionResult` we can combine the data load phase with the assignment phase into a single batch operation, at least on the surface. The aim is to make it easy to read and easier to write. + ## Returning Data `this.StartBatch()` returns a builder to define how you want GraphQL to construct your batch. We need to tell it how each of the child items we fetched maps to the parents that were supplied (if at all). diff --git a/docs/controllers/field-paths.md b/docs/controllers/field-paths.md index 3e8273e..9cbd147 100644 --- a/docs/controllers/field-paths.md +++ b/docs/controllers/field-paths.md @@ -3,8 +3,10 @@ id: field-paths title: Field Paths sidebar_label: Field Paths sidebar_position: 2 +hide_title: true --- +## What is a Field Path? GraphQL is statically typed. Each field in a query must always resolve to a single graph type known to the schema. This can make query organization rather tedious and adds A LOT of boilerplate code if you wanted to introduce even the slightest complexity to your graph. Let's think about this query: @@ -34,61 +36,72 @@ query { Knowing what we know about GraphQL's requirements, we need to create types for the grocery store, the bakery, pastries, a donut, the deli counter, meats, beef etc. Its a lot of setup for what basically boils down to two methods to retrieve a donut and a cut of beef by their respective ids. -This is where `virtual graph types` come in. Using a templating pattern similar to what we do with REST queries we can create rich graphs with very little boiler plate. Adding a new arm to your graph is as simple as defining a path to it in a controller. +Using a templating pattern similar to what we do with REST queries we can create rich graphs with very little boiler plate. Adding a new arm to your graph is as simple as defining a path to it in a controller. ```csharp title="Sample Controller" +// highlight-next-line [GraphRoute("groceryStore")] public class GroceryStoreController : GraphController { + // highlight-next-line [Query("bakery/pastries/donut")] public Donut RetrieveDonut(int id) {/* ...*/} + // highlight-next-line [Query("deli/meats/beef")] public Meat RetrieveCutOfBeef(int id) {/* ...*/} } ``` -Internally, for each encountered path segment (e.g. `bakery`, `meats`), GraphQL generates a `virutal graph type` to fulfill resolver requests for you and act as a pass through to your real code. It does this in concert with your real code and performs a lot of checks at start up to ensure that the combination of your real types as well as virutal types can be put together to form a functional graph. If a collision occurs the server will fail to start. +Internally, for each encountered path segment (e.g. `bakery`, `meats`), GraphQL generates a `intermediate graph type` to fulfill resolver requests for you and act as a pass through to your real code. It does this in concert with your real code and performs a lot of checks at start up to ensure that the combination of your real types as well as virutal types can be put together to form a functional graph. If a collision occurs the server will fail to start. -:::info Virtual Type Names +:::info Intermediate Type Names You may notice some object types in your schema named as `Query_Bakery`, `Query_Deli` these are the virtual types generated at runtime to create a valid schema from your path segments. ::: -#### Another Example +## Declaring Field Paths -You can nest fields as deep as you want and spread them across any number of controllers in order to create a rich organizational hierarchy to your data. This is best explained by code, take a look at these two controllers: +Declaring fields works just like it does with a REST query. You can nest fields as deep as you want and spread them across any number of controllers in order to create a rich organizational hierarchy to your data. This is best explained by code, take a look at these two controllers: ```csharp title="BakeryController.cs" +// highlight-next-line [GraphRoute("groceryStore/bakery")] public class BakeryController : GraphController { + // highlight-next-line [Query("pastries/search")] public IEnumerable SearchPastries(string nameLike) {/* ... */} + // highlight-next-line [Query("pastries/recipe")] - public Recipe RetrieveRecipe(int id) + public Task RetrieveRecipe(int id) {/* ... */} + // highlight-next-line [Query("breadCounter/orders")] public IEnumerable FindOrders(int customerId) {/* ... */} } ``` ```csharp title="PharmacyController.cs" +// highlight-next-line [GraphRoute("groceryStore/pharmacy")] public class PharmacyController : GraphController { + // highlight-next-line [Query("employees/search")] public IPastry SearchEmployees(string nameLike) {/* ... */} + // highlight-next-line [QueryRoot("pharmacyHours")] public HoursOfOperation RetrievePharmacyHours(DayOfTheWeek day) {/* ... */} + // highlight-next-line [Query("orders")] public IEnumerable FindOrders(int customerId) {/* ... */} @@ -133,76 +146,29 @@ With REST, this is probably 4 separate requests or one super contrived request b ## Actions Must Have a Unique Path -Each field in your object graph must uniquely map to one method (or getter property), commonly referred to as its resolver. +Each field in your object graph must uniquely map to one method or property getter; commonly referred to as its resolver. We can't declare a field twice. Take this example: -```csharp -public class BakeryController : GraphController -{ - [QueryRoot] - public Donut RetrieveDonutType(int id){/*...*/} -} - -public class Donut -{ - public int Id { get; set; } - public string Name { get; set; } - public Icing Icing { get; set; } -} - -public class Icing -{ - public IcingType Type { get; set; } - public string Name { get; set; } - public bool MegaSugar { get; set; } -} -``` -```graphql -query { - donut(id: 5){ - id - name - icing { - type - name - megaSugar - } - } -} -``` - -We can identify 8 unique fields: - -```javascript -[query] // the top level query -[query]/retrieveDonutType // the root level action method -[type]/donut/id // the id property of the donut object -[type]/donut/name -[type]/donut/icing -[type]/icing/type -[type]/icing/name -[type]/icing/megaSugar -``` - -But what about method overloading? - ```csharp title="Overloaded Methods" [GraphRoute("bakery")] public class BakeryController : GraphController { // Both Methods represent the same 'orderDonuts' field on the object graph + [Mutation] - public Manager OrderDonuts(int quantity){/*...*/} + // highlight-next-line + public BoxOfDonuts OrderDonuts(int quantity){/*...*/} [Mutation] - public Manager OrderDonuts(string type, int quantity){/*...*/} + // highlight-next-line + public BoxOfDonuts OrderDonuts(string type, int quantity){/*...*/} } ``` From a GraphQL perspective this equivilant to trying to define a `bakery` type with two fields named `orderDonuts`. Since both methods map to a field path of `[mutation]/bakery/orderDonuts` this would cause a `GraphTypeDeclarationException` to be thrown when your application starts. -With MVC the ASP.NET runtime could inspect any combinations of parameters passed on the query string or the POST body to work out which overload to call. You might be thinking, why can't GraphQL inspect the passed input arguments and make the same determination? +With Web API, the ASP.NET runtime could inspect any combinations of parameters passed on the query string or the POST body to work out which overload to call. You might be thinking, why can't GraphQL inspect the passed input arguments and make the same determination? Putting aside that it [violates the specification](http://spec.graphql.org/October2021/#sec-Objects), in some cases it probably could. But looking at this example we run into an issue: @@ -221,34 +187,30 @@ public class BakeryController : GraphController We'd pair these methods with different URL fragments and could work out which method to call in a REST request based on the full structure of the URL. -However, the GraphQL specification states that input arguments can be passed in any order [Spec § [2.6](https://graphql.github.io/graphql-spec/October2021/#sec-Language.Arguments)]. GraphQL, by definition, does not supply enough information in its query syntax to decide which overload to invoke. To combat the issue, the runtime will reject any field that it can't uniquely identify. +However, GraphQL states that input arguments can be passed in any order [Spec § [2.6](https://graphql.github.io/graphql-spec/October2021/#sec-Language.Arguments)]. By definition there is not enough information in the query syntax language to decide which overload to invoke. To combat the issue, the runtime will reject any field that it can't uniquely identify. No problem through, there are a number of ways fix the conflict. ### Declare Explicit Names -One approach is to declare explicit names for each of your methods. Not only does this resolve the conflict but should an errant refactor of your code occur, your graph fields won't magically be renamed to their new method names and break your front-end. +You can declare explicit names for each of your methods. Not only does this resolve the method overloading conflict but should an errant refactor of your code occur, your graph fields won't magically be renamed to their new method names and break your front-end. ```csharp title="Use Explicit Field Names" [GraphRoute("bakery")] public class BakeryController : GraphController { // GraphQL treats these fields differently! + + // highlight-next-line [Mutation("orderDonutsByQuantity")] public Manager OrderDonuts(int quantity){/*...*/} + // highlight-next-line [Mutation("orderDonutsByType")] public Manager OrderDonuts(string type, int quantity){/*...*/} } ``` -Now we have unique field paths: - -```javascript -[query]/bakery/orderDonutsByQuantity -[query]/bakery/orderDonutsByType -``` - But this can feel a bit awkward in some situations so instead... ### Change The Hierarchy @@ -259,11 +221,13 @@ Another alternative is to change where in the object graph the field sits. Here ```csharp title="Change the Field Path" [GraphRoute("bakery")] public class BakeryController : GraphController -{ +{ + // highlight-next-line [MutationRoot("orderDonuts")] public IEnumerable OrderDonuts(int count) {/*...*/} + // highlight-next-line [Mutation("orderDonuts")] public IEnumerable OrderDonuts( string type, @@ -292,13 +256,6 @@ mutation { } ``` -And the unique field paths are: - -```ruby -[mutation]/orderDonuts -[mutation]/bakery/orderDonuts -``` - ### Combine the Fields Lastly, we can make use of input objects with optional fields and combine parameters into a more robust method. @@ -307,7 +264,8 @@ Lastly, we can make use of input objects with optional fields and combine parame [GraphRoute("bakery")] public class BakeryController : GraphController { - [Mutation("orderDonuts")] + [Mutation("orderDonuts")] + // highlight-next-line public IEnumerable OrderDonuts(DonutOrderModel order) {/*...*/} } @@ -319,8 +277,6 @@ public class DonutOrderModel } ``` -Which can be called by either of these mutations: - ```graphql title="Sample Queries" mutation byQuantity { bakery{ diff --git a/docs/controllers/model-state.md b/docs/controllers/model-state.md index 8b2c155..4221872 100644 --- a/docs/controllers/model-state.md +++ b/docs/controllers/model-state.md @@ -5,27 +5,30 @@ sidebar_label: Model State sidebar_position: 1 --- -GraphQL, as a language, can easily enforce type level requirements like : +GraphQL, as a language, can easily enforce query level requirements like : -- The data must a collection -- The data cannot be null -- The data must be an integer +✅ The data must a collection.
+✅ The data value cannot be null.
+✅ The argument 'zipCode' must be supplied. -But it fails to enforce the individual business requirements of our data: +
-- Is the employee's last name less than 70 characters? -- Is the customer's phone number 7 or 10 digits? -- Is the number of donuts ordered at least 1? +But it fails to enforce the individual business requirements of application: -#### Model Validation to the Rescue +🧨 Is the employee's last name less than 70 characters?
+🧨 Is the customer's phone number 7 or 10 digits?
+🧨 Is the number of donuts ordered at least 1? -When your controller action is invoked the runtime will analyze the input parameters and will execute the validation attributes attached to each property to determine a validation state, just like you'd do in an MVC controller. +## Using Model Validation -In this example we use the `[Range]` attribute under `System.ComponentModel.DataAnnotations` to limit the quantity of donuts that can be ordered to two dozen. +When your controller action is invoked the runtime will analyze the input parameters and will execute the validation attributes attached to each object to determine its validations tate. This works just the same was as with a Web API controller. + +In this example we use the `[Range]` attribute under to limit the quantity of donuts that can be ordered to two dozen. ```csharp title="DonutOrderModel.cs" public class DonutOrderModel { + // highlight-next-line [Range(1, 24)] public int Quantity { get; set; } public string Type { get; set; } @@ -40,8 +43,10 @@ public class BakeryController : GraphController [MutationRoot("orderDonuts", typeof(CompletedDonutOrder))] public async Task OrderDonuts(DonutOrderModel order) { + // highlight-start if (!this.ModelState.IsValid) return this.BadRequest(this.ModelState); + // highlight-end var result = await _service.PlaceDonutOrder(order); return this.Ok(result); @@ -63,10 +68,16 @@ mutation { Just like with ASP.NET, `this.ModelState` contains an entry for each "validatiable" object passed to the method and its current validation state (valid, invalid, skipped etc.) along with all the rules that did not pass. Also, just like with ASP.NET you can define custom attributes that inherit from `ValidationAttriubte` and GraphQL will execute them as well. -In the example, we returned a IGraphActionResult to make use of `this.BadRequest()` which will add the friendly error messages to the outgoing response automatically. But we could have easily just returned null, thrown an exception or generated a generic custom error message. However you choose to deal with `ModelState` is up to you. GraphQL will validate the data but it doesn't take action when model validation fails. That's up to you. +In the example, we returned a IGraphActionResult to make use of `this.BadRequest()` which will add the friendly error messages to the outgoing response automatically. But we could have easily just returned null, thrown an exception or generated a generic custom error message. However you choose to deal with `ModelState` is up to you. + +:::note +GraphQL will validate the data but it doesn't take action when model validation fails. That's up to you. +::: + +
-#### _Implementation Note_ +⚠️ **Implementation Note** -GraphQL makes use of the same `System.ComponentModel.DataAnnotations.Validator` that ASP.NET does to validate its input objects. [All the applicable rules](https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-7.0) that apply to MVC model validation also apply to GraphQL. +GraphQL makes use of the same `System.ComponentModel.DataAnnotations.Validator` that ASP.NET does to validate its input objects. [All the applicable rules](https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-7.0) that apply to Web API model validation also apply to GraphQL. -However, where MVC will validate model binding rules and represent binding errors it its ModelState object, GraphQL will not. GraphQL binding issues such as type expressions and nullability are taken care of at the query level, long before a query plan is finalized and the action method is invoked. As a result, the model state of GraphQL ASP.NET is a close approximation of MVC's model state object, but it is not a direct match. +However, where Web API will validate model binding rules and represent binding errors it its ModelState object (such as invalid or missing property names) GraphQL will not. GraphQL binding issues such as type expressions and nullability are taken care of at the query level, long before a query plan is finalized and the action method is invoked. The model state of GraphQL ASP.NET is a close approximation of Web API's model state object, but it is not an exact match. \ No newline at end of file diff --git a/docs/controllers/type-extensions.md b/docs/controllers/type-extensions.md index fb1988a..5010bc6 100644 --- a/docs/controllers/type-extensions.md +++ b/docs/controllers/type-extensions.md @@ -7,12 +7,13 @@ sidebar_position: 4 ## Working with Child Data -Before we dive into `type extensions` we have to talk about parent-child relationships. So far, the examples we've seen have used well defined fields in an object graph. Be that an action method on a controller or a property on an object. But when we think about real world data, there are scenarios where that poses a problem. Lets suppose for a moment we have a chain of bakery stores that let customers place orders for cakes at an individual store and customize the writing on the cake. +Before we dive into type extensions we have to talk about parent-child relationships. So far, the examples we've seen have used well defined fields in an object graph. Be that an action method on a controller or a property on an object. But when we think about real world data, there are scenarios where that poses a problem. Lets suppose for a moment we have a chain of bakery stores that let customers place orders for cakes at an individual store and customize the writing on the cake. ```csharp title="Sample Bakery Model" public class Bakery { public int Id { get; set; } + // highlight-next-line public List Orders { get; set; } } @@ -20,18 +21,19 @@ public class CakeOrder { public Customer Customer { get; set; } public string WrittenPhrase { get; set; } + // highlight-next-line public Bakery Bakery { get; set; } } // ...Customer class excluded for brevity ``` -Given what we've seen so far: +But consider the following scenarios: - What happens when we retrieve a single `CakeOrder` via a controller? - Do we automatically have to populate the entire `Bakery` and `Customer` objects? - Even if a caller didn't request any of that data? -- What about retrieving a bakery that may have 1000s of cake orders? +- What happens when retrieving a bakery that may have 1000s of cake orders? Our application is going to slow to a crawl very quickly doing all this extra data loading. In the case of a single Bakery, a timeout may occur trying to fetch many years of cake orders to populate the bakery instance from a database query only to discard them when a graphql query doesn't ask for it. If we're using something like Entity Framework how do we know when to use an Include statement to populate the child data? (Hint: you don't) @@ -65,7 +67,7 @@ Well that's just plain awful. We've over complicated our bakery model and made i ## The [TypeExtension] Attribute -We've talked before about GraphQL maintaining a 1:1 mapping between a field in the graph and a method to retrieve data for it (i.e. its assigned resolver). What prevents us from creating a method to fetch a list of Cake Orders and saying, "Hey, GraphQL! When someone asks for the field `[type]/bakery/orders` call our method instead of a property getter on the `Bakery` class. As it turns out, that is exactly what a `Type Extension` does. +We've talked before about GraphQL maintaining a 1:1 mapping between a field in the graph and a method to retrieve data for it (i.e. its assigned resolver). What prevents us from creating a method to fetch a list of Cake Orders and saying, "Hey, GraphQL! When someone asks for a set of bakery orders call a custom method instead of a property getter on the `Bakery` class. As it turns out, that is exactly what a `Type Extension` does. ```csharp title="Bakery Type Extension" public class Bakery @@ -79,10 +81,11 @@ public class BakedGoodsCompanyController : GraphController [QueryRoot("bakery")] public Bakery RetrieveBakery(int id){/*...*/} - // declare the type extension as a controller action. + // declare a extension to the Bakery object + // highlight-next-line [TypeExtension(typeof(Bakery), "orders")] - public async Task> RetrieveCakeOrders(Bakery bakery, int limitTo = 15){ - + public async Task> RetrieveCakeOrders(Bakery bakery, int limitTo = 15) + { return await _service.RetrieveCakeOrders(bakery.Id, limitTo); } } @@ -94,7 +97,7 @@ There is a lot to unpack here, so lets step through it: - We've declared the `RetrieveBakery` method as a root field named `bakery` that allows us to fetch a single bakery. - We've added a method named `RetrieveCakeOrders`, declared it as an _extension_ to the `Bakery` graph type and gave it a field name of `orders`. -- The method returns `List` as the type of data it generates. +- The extension returns `List` as the type of data it generates. - The method takes in a `Bakery` instance (more on that in a second) as well as an integer, with a default value of `15`, to limit the number of orders to retrieve. Now we can query the `orders` field from anywhere a bakery is returned in the object graph and GraphQL will invoke our method: @@ -115,29 +118,27 @@ query { Type Extensions allow you to attach new fields to a graph type without altering the original `System.Type`. ::: -#### But what about the Bakery parameter? - -When you declare a type extension it will only be invoked in context of the type being extended. +#### ❓ But what about the Bakery parameter? -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. +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 your method and lets you figure out what needs to happen to resolve the field. -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)`. +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)`. Other parameters are not effected. 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 Security](./authorization) is also wired up for us. -- The bakery model is greatly simplified. +✅ 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 Security](./authorization) and use of the `[Authorize]` attribute is also wired up for us.
+✅The bakery model is greatly simplified. -### Can every field be a type extension? +## Can Every Field be a Type Extension? 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 data result. -- Six separate database queries, one for each string value requested. +- Six separate database queries, one for each column 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. 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. +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 were already querying extra data and they were likely transmitting that data to the client. -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. +It comes down to your use case. There are times when it makes sense to seperate things 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/entity-framework.md b/docs/development/entity-framework.md index 45eccec..a6c70b8 100644 --- a/docs/development/entity-framework.md +++ b/docs/development/entity-framework.md @@ -55,13 +55,10 @@ One way to correct this problem is to register your DbContext as a transient object. ```csharp title="Register DbContext as Transient" -public void ConfigureServices(IServiceCollection services) -{ - services.AddDbContext(o => - { - o.UseSqlServer(""); - }, ServiceLifetime.Transient); -} +services.AddDbContext(o => + { + o.UseSqlServer(""); + }, ServiceLifetime.Transient); ``` Now each controller instance will get its own DbContext and the queries can execute in parallel without issue. diff --git a/docs/development/unit-testing.md b/docs/development/unit-testing.md index c290173..5e4cd79 100644 --- a/docs/development/unit-testing.md +++ b/docs/development/unit-testing.md @@ -43,7 +43,7 @@ public async Task MyController_InvocationTest() 1. Mock the query execution context (the object that the runtime acts on) using `.CreateQueryContextBuilder()` 2. Configure the text, variables etc. on the builder. 3. Build the context and submit it for processing: - - Use `server.ExecuteQuery()` to process the context. `context.Result` will be filled with the final `IGraphOperationResult` which can be inspected for resultant data fields and error messages. + - Use `server.ExecuteQuery()` to process the context. `context.Result` will be filled with the final `IQueryExecutionResult` which can be inspected for resultant data fields and error messages. - Use `server.RenderResult()` to generate the json string a client would recieve if they performed the query. diff --git a/docs/execution/malicious-queries.md b/docs/execution/malicious-queries.md index 8d184f1..763f20a 100644 --- a/docs/execution/malicious-queries.md +++ b/docs/execution/malicious-queries.md @@ -47,7 +47,9 @@ services.AddGraphQL(options => }); ``` -> The default value for `MaxQueryDepth` is `null` or no limit. +:::info Default Max Query Depth +The default value for `MaxQueryDepth` is `null` (i.e. no limit). +::: ## Query Complexity @@ -68,7 +70,7 @@ query PhoneManufacturer { } ``` -It would not be far fetched to assume that this phone manufacturer has at least 500 parts in their inventory and that those parts might be sourced from 2-3 individual suppliers. If that's the case our result is going to contain 3000 field resolutions (500 parts \* 3 suppliers \* 2 fields per supplier) just to show the name and address of each supplier. Thats a lot of data!!!! What if we added order history per supplier? Now we'd looking at 100,000+ results. The take away here is that your field resolutions can balloon quickly if you're not careful. +It would not be far fetched to assume that this phone manufacturer has at least 500 parts in their inventory and that those parts might be sourced from 2-3 individual suppliers. If that's the case our result is going to contain 3000 field resolutions (500 parts \* 3 suppliers \* 2 fields per supplier) just to show the name and address of each supplier. Thats a lot of data!!!! What if we added order history per supplier? Now we'd looking at 100,000+ results. The take away here is that your field resolutions can balloon quickly, even on small queries, if you're not careful. While this query only has a field depth of 3, `allParts > suppliers > name`, the performance implications are much more impactful than the bakery in the first example because of the type of data involved. (Side note: this is a perfect example where a [batch operation](../controllers/batch-operations) would improve performance exponentially.) @@ -81,11 +83,11 @@ services.AddGraphQL(options => }); ``` -> The default value for `MaxQueryComplexity` is `null` or no limit. +:::info Default Max Complexity +The default value for `MaxQueryComplexity` is `null` (i.e. no maximum). +::: -There is no magic bullet for choosing a maximum value as its going to be largely dependent on your data and how customers query it. - -## Calculating Query Complexity +### Calculating Query Complexity After a query plan is generated, the chosen operation is inspected and weights are applied to each of the fields then summed together to generate a final score. @@ -93,15 +95,13 @@ A complexity score is derived from these attributes: | Attribute | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------- | -| Operation Type | This refers to the operation as a whole being a `mutation` or a `query`. | -| Execution Mode | Whether or not a field is being executed as a batch operation or per source item. | -| Resolver Type | Is the field targeting a controller action, an object property or an object method? | -| Type Expression | Does the field produce 1 single item or a collection of items | +| Operation Type | This refers to the operation being a `mutation` or a `query`. Mutations are weighted more than queries.| +| Execution Mode | Whether or not a given field is being executed as a batch operation or "per source item". | +| Resolver Type | The type of resolver being invoked. For example, controller actions are weighted more heavily than simple property resolvers. | +| Type Expression | Does the field produce 1 single item or a collection of items? | | Complexity Factor | A user controlled value to influence the calculation for queries or mutations that are particularly long running | -The `estimated complexity` of the query plan is the operation with the highest individual score. - -The code for calculating the value can be seen in [`DefaultOperationComplexityCalculator`](https://github.com/graphql-aspnet/graphql-aspnet/blob/master/src/graphql-aspnet/Defaults/DefaultOperationComplexityCalculator%7BTSchema%7D.cs) +The code for calculating the value can be seen in [`DefaultOperationComplexityCalculator`](https://github.com/graphql-aspnet/graphql-aspnet/blob/master/src/graphql-aspnet/Engine/DefaultOperationComplexityCalculator%7BTSchema%7D.cs) ### Setting a Complexity Weight @@ -113,6 +113,7 @@ The attributes `[GraphField]`, `[Query]`, `[Mutation]`, `[QueryRoot]`, `[Mutatio public class BakeryController : GraphController { // Complexity is a float value + // highlight-next-line [QueryRoot(Complexity = 1.3)] public Donut RetrieveDonutType(int id){/*...*/} } @@ -122,9 +123,16 @@ public class BakeryController : GraphController - A factor less than 1 will decrease the weight - The minimum value is `0` and the default value is `1` +Complexity scores that do not exceed the limit are written to `QueryPlanGenerated (EventId: 86400)`, a debug level event, after the query plan is successfully generated. Complexity scores that do exceed the limit are written directly to the errors collection on the query response. + + +:::tip Profile Your Queries +There is no magic bullet for choosing complexity values or setting a maximum allowed value as its going to be largely dependent on your data and how customers query it. Spend time profiling your queries, investigate their calculated complexities and act accordingly. +::: + ## Implement Your Own Complexity Calculation -You can override how GraphQL calculates the complexity of any given query operation. Implement `IQueryOperationComplexityCalculator` and inject it into your DI container before calling `.AddGraphQL()`. +You can override how the library calculates the complexity of any given query operation. Implement `IQueryOperationComplexityCalculator` and inject it into your DI container before calling `.AddGraphQL()`. This interface has one method where `IGraphFieldExecutableOperation` represents the collection of requested fields contexts along with the input arguments, child fields and directives that are about to be executed: diff --git a/docs/introduction/made-for-aspnet-developers.md b/docs/introduction/made-for-aspnet-developers.md index 2db3e60..fabef40 100644 --- a/docs/introduction/made-for-aspnet-developers.md +++ b/docs/introduction/made-for-aspnet-developers.md @@ -5,7 +5,7 @@ sidebar_label: Made for ASP.NET Developers sidebar_position: 1 --- -This library is designed by people who use [ASP.NET](https://dotnet.microsoft.com/en-us/apps/aspnet) in their day to day activities and built for similar minded developers. When you first started digging in to GraphQL you most likely came across the plethora of [articles](https://www.graphqlweekly.com/), [documents](https://en.wikipedia.org/wiki/GraphQL), [tutorials](https://www.howtographql.com/) and [groups](https://www.apollographql.com/) centered around JavaScript. JavaScript certainly has the highest adoption rate and with the tools provided by [Apollo](https://www.apollographql.com/) its no surprise. Its amazing how well those tools fit in with the existing knowledge and coding paradigms of JavaScript developers on both sides of the fence (be that front end or back end). +This library is designed by people who use [ASP.NET](https://dotnet.microsoft.com/en-us/apps/aspnet) in their day to day activities and is built for similar minded developers. When you first started digging in to GraphQL you most likely came across the plethora of [articles](https://www.graphqlweekly.com/), [documents](https://en.wikipedia.org/wiki/GraphQL), [tutorials](https://www.howtographql.com/) and [groups](https://www.apollographql.com/) centered around JavaScript. JavaScript certainly has the highest adoption rate and with the tools provided by [Apollo](https://www.apollographql.com/) its no surprise. Its amazing how well those tools fit in with the existing knowledge and coding paradigms of JavaScript developers on both sides of the fence (be that front end or back end). We believe that tooling and workflow is everything when it comes to picking up a technology. Its much more difficult for you (or your team) to adopt something new if there is no connection to what you already know. Migrating your personal development efforts or an entire team from .NET to NodeJS to leverage Apollo Server, for instance, is hard. The learning curve and even the monetary cost of bringing a team up to speed is high. But if you can leverage existing skills you reduce that cost significantly. @@ -13,12 +13,11 @@ We believe that tooling and workflow is everything when it comes to picking up a This is a core, guiding principle for the development of this library. We aim to reuse what you know. Or if you are still learning, make what you learn transferable to other .NET technologies. When coming from a .NET background, being able to reason about your graph queries in terms of `Controllers` and `Actions` eases the cognitive load as you transition to thinking in terms of Fields and object graphs. -Using familiar concepts like _Binding Models_ and _View Models_; commonly used attributes like `[Authorize]`, `[Required]`, `[StringLength]`; modern ASP.NET's abstraction concepts like `IServiceCollection`, `ILogger`, and `Startup.cs` all play a part in hopes to give you a familiar programming model that you can start using immediately without reinventing too many wheels. +Using familiar concepts like _Binding Models_ and _View Models_; commonly used attributes like `[Authorize]`, `[Required]`, `[StringLength]`; modern ASP.NET's abstraction concepts like `IServiceCollection` and `ILogger` all play a part in hopes of giving you a familiar programming model that you can start using immediately without reinventing too many wheels. -Take, for instance, this controller and a sample query that would call it. Can you tell what it does? If you are familiar with ASP.NET MVC then the answer is probably yes! +Take, for instance, this controller and a sample query that would call it. Can you tell what it does? If you are familiar with Web API then the answer is probably yes! ```cs title="PersonController.cs" -// C# public class PersonController: GraphController { private IPersonService _service; @@ -45,7 +44,7 @@ query { } ``` -Another consideration when trying to implement GraphQL in .NET is the amount of boiler plate code required. Since C# is a strongly typed language the volume of additional coding required to generate an object graph tends to be high. Many libraries take the approach of ultimate flexibility, requiring you to completely code your object graph (the fields that can be queried) and individually map all model properties and resolver methods manually. +Another consideration when trying to implement GraphQL in .NET is the amount of boiler plate code required. Since C# is a strongly typed language the volume of additional coding required to generate an object graph tends to be high, especially in larger graphs. Many libraries take the approach of ultimate flexibility, requiring you to completely code your object graph, and all the fields that can be queried, and individually map all model properties and resolver methods manually. To address this, GraphQL ASP.NET has adopted an opinionated approach to its implementation. It makes some minor assumptions about how you will deliver your data in exchange for some much needed code generation and specification support. If the code in the controller above makes sense and feels natural to you; then this library might be worth a look. In terms of GraphQL, this single controller will: @@ -55,24 +54,28 @@ To address this, GraphQL ASP.NET has adopted an opinionated approach to its impl The library will automatically wire up your graph controllers and scan your model objects. There is no additional, required configuration. When you add a new controller, new actions or new model properties they are automatically injected everywhere that object is used. -Are you working on a large project that has shared assemblies between services? No problem, you can direct GraphQL on where to look for controllers and model objects or even be explicit in what you want it to consume...down to the property level. +Are you working on a large project that has shared assemblies between services? No problem, you can direct GraphQL on where to look for controllers and model objects or even be explicit in what you want it to consume...down to the individual property level. -## Plays Nice with MVC Controllers, Razor Views and Razor Pages +:::note You're in control + Out of the box the library tries to pick the route of least resistance, but there are many ways to control what classes, enums etc. are included (or excluded) in your object graph. +::: -This library is an extension on the standard ASP.nET pipeline, not a replacement. At its core, a graphql query is just another route on your application. At startup it registers a middleware component to handle requests using `appBuilder.Map()`. +## Plays Nice with Web API Controllers, Razor Views and Razor Pages -Also, if you are integrating into an existing project, you'll find a lot of your utility code will work out of the box which should ease your migration. Any existing services, custom Authorization and Validation attributes etc. can be directly attached to graph action methods and input models. You might even find that most of your model objects work as well. +This library sits as an extension on the standard ASP.NET pipeline, not a replacement. At its core, a graphql query is just another GET or POST route on your application. At startup it registers a middleware component to handle requests using `appBuilder.Map()`. + +Also, if you are integrating into an existing project, you'll find a lot of your utility code should work out of the box which should ease your migration. Any existing services, custom authorization and model validation attributes etc. can be directly attached to graph action methods and input models. You might even find that most of your model objects work as well. ## Scoped Dependency Injection -Services are injected into graph controllers in the same manner as ASP.nET controllers and with the same scope resolution. +Services are injected into graph controllers in the same manner as ASP.NET controllers and with the same scope resolutions. ## User Authorization -The user model is exactly the same. In fact, the `ClaimsPrincipal` passed to `this.User` on an MVC controller is forwarded to GraphQL and used to validate any `[Authorize]` attributes on your graph controller actions. Internally, it uses the same `IAuthorizationService` that gets added when you call `services.AddAuthorization()` in `Startup.cs`. +The user model is exactly the same. In fact, the `ClaimsPrincipal` passed to `this.User` on a Web API controller is the same instance used to validate any `[Authorize]` attributes on your graph controller actions. Internally, it uses the same `IAuthorizationService` that gets added when you call `services.AddAuthorization()` during startup. ## Custom Action Results -Many teams define custom action results beyond `this.Ok()` and `this.BadRequest()` to centralize how they will respond to requests on their Web API controllers to provide consistent messaging, perform some sort of logging or create a common return payload. GraphQL ASP.NET supports this model as well. Out of the box you get support for many of the relevant action results around returning data, indicating an error or denying access, but you can implement your own `IGraphActionResult` to standardize how a given result is converted into a response object and used by the runtime. This includes control to invalidate the field, inject customized error messages or cancel the field request altogether. +Many teams define custom action results beyond `this.Ok()` and `this.BadRequest()` to standardize how they will respond to requests on their Web API controllers to provide consistent messaging, perform some sort of logging or create a common return payload. GraphQL ASP.NET supports this model as well. Out of the box you get support for many of the relevant action results around returning data, indicating an error or denying access, but you can implement your own `IGraphActionResult` to standardize how a given result is converted into a response object and used by the runtime. This includes control to invalidate the field, inject customized error messages or cancel the field request altogether. _Side Note:_ Not all action results make sense in GraphQL. For instance, you won't find a way to download a file or indicate a 204 (no content) result. A GraphQL field must always return a piece of data (even if its null). Since the `IGraphActionResult` object is only a small piece in an entire query of many fields, its scope of abilities is paired to match. diff --git a/docs/introduction/what-is-graphql.md b/docs/introduction/what-is-graphql.md index 1afc74d..d1aa259 100644 --- a/docs/introduction/what-is-graphql.md +++ b/docs/introduction/what-is-graphql.md @@ -53,7 +53,7 @@ One of the primary benefits of GraphQL is the requestor only gets the data they ## GraphQL is not a .NET Technology -GraphQL is a [specification](https://graphql.github.io/graphql-spec/) , a contract, describing a syntax and rule set for how to process requests for data. There are many implementations of GraphQL for various tech stacks from JavaScript, to Java, to Ruby and even other .NET implementations to choose from. +GraphQL is a query [language specification](https://spec.graphql.org/); a contract describing a syntax and rule set for how to process requests for data. There are many implementations of GraphQL for various tech stacks from JavaScript, to Java, to Ruby and even other .NET implementations to choose from. #### Other Popular GraphQL Implementations @@ -62,7 +62,7 @@ GraphQL is a [specification](https://graphql.github.io/graphql-spec/) , a contra | [Apollo Server](https://github.com/apollographql/apollo-server) | JavaScript | | [GraphQL Ruby](https://graphql-ruby.org/) | Ruby | | [GraphQL Java](https://www.graphql-java.com/) | Java | -| [GraphQL .NET](https://github.com/graphql-dotnet/graphql-dotnet) | .NET | +| [GraphQL .NET](https://github.com/graphql-dotnet/graphql-dotnet)| .NET | | [Hot Chocolate](https://github.com/ChilliCream/hotchocolate) | .NET | ## Why Choose GraphQL? diff --git a/docs/logging/standard-events.md b/docs/logging/standard-events.md index 9e3d346..37525b2 100644 --- a/docs/logging/standard-events.md +++ b/docs/logging/standard-events.md @@ -25,7 +25,7 @@ _Constants for all log entry properties can be found at_ `GraphQL.AspNet.Logging ### 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 recorded once per application instance. +This event is recorded when GraphQL successfully registers an entry in the ASP.NET route table to accept requests for a target schema. This event is recorded once per application instance. **Important Properties** @@ -68,7 +68,7 @@ This is event is recorded when the query execution pipeline first receives a new | Property | Description | | -------------------- | -------------------------------------------------------------- | | _Username_ | the value of `this.User.Identity.Name` or null | -| _OperationRequestId_ | A unique id identifying the overall request that was received. | +| _QueryRequestId_ | A unique id identifying the overall request that was received. | | _QueryText_ | The query provided by the user. | ### Query Plan Generated @@ -129,7 +129,7 @@ This is event is recorded when the final result for the request is generated and | Property | Description | | -------------------- | ------------------------------------------------------------------------------------- | -| _OperationRequestId_ | A unique id identifying the overall request. | +| _QueryRequestId_ | 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. | @@ -142,7 +142,7 @@ This is event is recorded when the a request is explicitly cancelled, usually by | Property | Description | | -------------------- | ------------------------------------------------------------------------------------- | -| _OperationRequestId_ | A unique id identifying the overall request. | +| _QueryRequestId_ | A unique id identifying the overall request. | | _TotalExecutionMs_ | A numerical value indicating the total runtime of the request, in milliseconds. | @@ -154,7 +154,7 @@ This is event is recorded when the a request is is cancelled due to reaching a m | Property | Description | | -------------------- | ------------------------------------------------------------------------------------- | -| _OperationRequestId_ | A unique id identifying the overall request. | +| _QueryRequestId_ | A unique id identifying the overall request. | | _TotalExecutionMs_ | A numerical value indicating the total runtime of the request, in milliseconds. | @@ -341,7 +341,7 @@ This event is recorded by the controller if it is unable to invoke the target ac ### Action Unhandled Exception -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. +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** diff --git a/docs/logging/subscription-events.md b/docs/logging/subscription-events.md index 5e34d8e..84d3184 100644 --- a/docs/logging/subscription-events.md +++ b/docs/logging/subscription-events.md @@ -26,7 +26,7 @@ This event is recorded when the server's schema-agnostic, internal dispatch queu ### 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 +This event is recorded when GraphQL successfully registers an entry in the ASP.NET route table to accept requests for a target schema as well as register the middleware component necessary to receive websocket requests. **Important Properties** diff --git a/docs/quick/_category_.json b/docs/quick/_category_.json index f0a2369..779779e 100644 --- a/docs/quick/_category_.json +++ b/docs/quick/_category_.json @@ -1,5 +1,5 @@ { - "label": "Jump In!", + "label": "Getting Started", "position": 0, "collapsed": false } \ No newline at end of file diff --git a/docs/quick/code-examples.md b/docs/quick/code-examples.md index 6f17001..06d3720 100644 --- a/docs/quick/code-examples.md +++ b/docs/quick/code-examples.md @@ -5,13 +5,8 @@ sidebar_label: Code Examples sidebar_position: 2 --- -This page shows a quick introduction to sample graphql queries and the C# code to support. If you need a more complete walk through the links to the left have every thing you need. +This page shows a quick introduction to some common scenarios and the C# code to support. -These are a great place to start: - -🔗 [Why Choose GraphQL ASP.NET](../introduction/made-for-aspnet-developers) - -🔗 [Start a new GraphQL ASP.NET Project](./quick-start) ## A Basic Controller @@ -69,7 +64,7 @@ query { } ``` -:::tip Did you notice? +:::info Did you notice? In the query the hero field is `camelCased` but in C# the method is `ProperCased`? GraphQL ASP.NET automatically translates your names appropriately to standard GraphQL conventions. The same goes for your graph type names, enum values etc. You can implement your own `GraphNameFormatter` and alter the name formats for each of your registered schemas. @@ -83,7 +78,7 @@ If your models share a common interface just return it from a controller action ```csharp title="HeroController.cs" public class HeroController : GraphController { - [QueryRoot] + [QueryRoot(typeof(Droid), typeof(Human))] public ICharacter Hero(Episode episode) { if(episode == Episode.Empire) @@ -108,18 +103,6 @@ public class HeroController : GraphController } ``` -```csharp title="Interfaces.cs" -// Properties omitted for brevity -public interface ICharacter -{/*...*/} - -public class Human : ICharacter -{/*...*/} - -public class Droid : ICharacter -{/*...*/} -``` - ```graphql title="GraphQL Query" query { hero(episode: JEDI) { @@ -144,8 +127,7 @@ We've used `[QueryRoot]` so far to force a controller action to be a root field ```csharp title="RebelAllianceController.cs" [GraphRoute("rebels")] -public class RebelAllianceController - : GraphController +public class RebelAllianceController : GraphController { [Query("directory/hero")] public Human RetrieveHero(Episode episode) @@ -161,8 +143,7 @@ public class RebelAllianceController } ``` -```graphql title="Query" -// GraphQL Query +```graphql title="Sample Query" query { rebels { directory { @@ -189,8 +170,6 @@ public class PersonsController : GraphController _personService = service; } - // your C# method name and the graph field name - // can be different [QueryRoot("person")] public async Task RetrievePerson(int id) { @@ -237,8 +216,7 @@ public class PersonsController : GraphController } ``` -```graphql title="Query" -// GraphQL Query +```graphql title="Sample Query" query { self { id @@ -256,7 +234,7 @@ query { ## Mutations & Model State -GraphQL ASP.NET will automatically enforce the query specification rules for you, but that doesn't help for business-level requirements like string length or integer ranges. For that, it uses the familiar goodness of `ValidationAttribute` (meaning everything under `System.ComponentModel.DataAnnotations`). +GraphQL ASP.NET will automatically enforce the query specification rules for you, but that doesn't help for business-level requirements like string length or integer ranges. For that, it uses the familiar goodness of Validation Attributes (e.g. `[StringLength]`, `[Range]` etc.). ```csharp title="PersonsController.cs" @@ -291,7 +269,7 @@ public class Human } ``` -```graphql title="Query" +```graphql title="Sample Query" mutation { joinTheResistance( newPerson: { @@ -309,19 +287,16 @@ We used `Human` as an input argument and as the returned data object. GraphQL AS ::: -## Custom Action Results +## Action Results Just as Web API makes use of `IActionResult` to perform post processing on the result of a controller method, GraphQL ASP.NET makes use of `IGraphActionResult`. -Reusing the previous example, here we make use of `this.BadRequest()` to automatically generate an appropriate error message inside the response payload's `errors` property when model validation fails. Field origin information including the path array and line/column number of the original query are wired up automatically. - +Reusing the previous example, here we make use of `this.BadRequest()` to automatically generate an appropriate error message in the response when model validation fails. Field origin information including the path array and line/column number of the original query are wired up automatically. ```csharp // C# Controller public class PersonsController : GraphController { - /* constructor hidden for brevity */ - [MutationRoot("joinTheResistance")] public async IGraphActionResult CreatePerson(Human model) { @@ -349,3 +324,7 @@ public class Human public string HomePlanet { get; set; } } ``` + +:::note GraphQL is not Rest + Unlike WebAPI, `BadRequest()` doesn't generate a HTTP Status 400 error for the request. If there are multiple controller methods being resolved GraphQL can still generate a partial response and render data for other parts of the query. Most "error" related action results add a standard error message to the result with different reason codes. +::: \ No newline at end of file diff --git a/docs/quick/overview.md b/docs/quick/overview.md index bf32950..5b00645 100644 --- a/docs/quick/overview.md +++ b/docs/quick/overview.md @@ -3,60 +3,118 @@ id: overview title: Overview sidebar_label: Overview sidebar_position: 0 -description: A quick overview of this documentation and how to use it. -hide_title: true +description: A quick overview of how to use the library --- -:::info This project is currently in open beta - Some features of the library may change prior to a final release. -::: -## How To Use This Documentation +```powershell title="Install The Library" +# Using the dotnet CLI +> dotnet add package GraphQL.AspNet --prerelease -This documentation is best used as a reference guide for the various features of GraphQL ASP.NET but it helps to read through a few sections to get an understanding of the core concepts. +# using Package Manager Console +> Install-Package GraphQL.AspNet -IncludePrerelease +``` -✅ [Controllers](../controllers/actions) - -An overview on how to build a controller and define an action method. +## Documentation -✅ [Attributes](../reference/attributes) - A reference to all the attributes supported by GraphQL ASP.NET. Attributes are used extensively to annotate and configure your controllers and model classes. +This documentation should can be used as a reference for various aspects of the library or read to discover the various features of the library. If you have questions don't hesitate to ask over on [Github](https://github.com/graphql-aspnet/graphql-aspnet). -✅ [Schema Configuration](../reference/schema-configuration) - A reference to the various configuration options for your schema and how they affect the runtime. -#### Target Audience +## Helpful Pages + -This documentation serves as part reference and part tutorial for GraphQL ASP.NET. You should have a familiarity with GraphQL and ASP.NET MVC. We won't spend a lot of time covering core concepts such as how ASP.NET controllers operate or the ends and outs of authorization. Many implementation details are shown in terms of code examples as well and without a familiarity with MVC, things may not always be so clear. +📌 [Demo Projects](../reference/demo-projects.md) : A number of downloadable sample projects covering a wide range of topics -Here are some good starting points for learning more about GraphQL or ASP.NET MVC before diving in to GraphQL ASP.NET. +💡 [Controllers](../controllers/actions.md) : Everything you need to know about `GraphController` and defining action methods. -[**Learn GraphQL**](https://graphql.org/learn/) - A walk through on the query language by the GraphQL team +📜 [Attributes](../reference/attributes.md) : A reference list of the various `[Attributes]` used by GraphQL ASP.NET to create your schema. -[**Comparing GraphQL and REST**](https://blog.apollographql.com/graphql-vs-rest-5d425123e34b) - A helpful comparison by the Apollo Team +📐 [Schema Configuration](../reference/schema-configuration.md) : A reference list of the various configuration options available at application startup. -[**Getting started with ASP.NET Core MVC**](https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/start-mvc?view=aspnetcore-5.0&tabs=visual-studio) - Scaffolding a .NET ASP.NET Core MVC app from the ground up. + -## Key Terms +## Building Your First Application -This documentation uses a number of terms to refer to the various pieces of the library: +### Create a new Web API Project +💻 Setup a new `ASP.NET Core Web API` project: -### Schema -This is the set of data types, their fields, input arguments etc. that are exposed on an object graph. When you write a graphql query to return data the fields you request, their arguments and their children must all be defined on a schema that graphql will validate your query against. +![web api project](../assets/create-new-web-api-project.png) -> In GraphQL ASP.NET the schema is generated at runtime directly from your C# controllers; there is no additional boilerplate code necessary to define a schema. +### Add the Package From Nuget +💻 Add the `GraphQL.AspNet` nuget package: -Your schema is "generated" at runtime by analyzing your model classes, controllers and action methods then populating a `GraphSchema` container with the appropriate graph types to map graphql requests to your controllers. +```powershell +# Powershell terminal, Package Manager in Visual Studio, Developer Command Prompt etc. +> dotnet add package GraphQL.AspNet --prerelease +``` -### Fields & Resolvers -In GraphQL terms, a field is any requested piece of data (such as an id or name). A resolver fulfills the request for data from a schema field. It takes in a set of input arguments and produces a piece of data that is returned to the client. In GraphQL ASP.NET your controller methods act as resolvers for top level fields in any query. +### Create a Controller -### Graph Type +💻 Create your first Graph Controller: -A graph type is an entity on your object graph; a droid, a donut, a string, a number etc. In GraphQL ASP.NET your model classes, interfaces, enums, controllers etc. are compiled into the various graph types required by the runtime. +```csharp title="BakeryController.cs" +using GraphQL.AspNet.Attributes; +using GraphQL.AspNet.Controllers; -#### Root Graph Types -There are three root graph types in GraphQL: Query, Mutation, Subscription. Whenever you make a graphql request, you always specify which query root you are targeting. This documentation will usually refer to all operations as "queries" but this includes mutations and subscriptions as well. +public class BakeryController : GraphController +{ + [QueryRoot("donut")] + public Donut RetrieveDonut() + { + return new Donut() + { + Id = 3, + Name = "Snowy Dream", + Flavor = "Vanilla" + }; + } +} -### Query Document -This is the raw query string submitted by a client. When GraphQL accepts a query it is converted from a string to an internal document format that is parsed and used to fulfill the request. +public class Donut +{ + public int Id { get; set; } + public string Name { get; set; } + public string Flavor { get; set; } +} +``` -> Queries, Mutations and Subscriptions are all types of query documents. +### Configure Startup + +💻 Register GraphQL with your services collection and your application pipeline: + +```csharp title="Program.cs" +using GraphQL.AspNet.Configuration; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// highlight-next-line +builder.Services.AddGraphQL(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +// highlight-next-line +app.UseGraphQL(); +app.Run(); +``` + + +### Execute a Query + +💻 Start the application and using your favorite tool, execute a query: + +```graphql title="Sample Query" +query { + donut { + id + name + flavor + } +} +``` + +#### Results: + +![query results](../assets/overview-sample-query-results.png) \ No newline at end of file diff --git a/docs/quick/quick-start.md b/docs/quick/quick-start.md deleted file mode 100644 index 57b55f6..0000000 --- a/docs/quick/quick-start.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -id: quick-start -title: Quick Start Guide -sidebar_label: Quick Start -sidebar_position: 1 ---- - -This guide will help you get a GraphQL project up and running so you can start experimenting. We'll cover the following: - -1. Create a new ASP.NET MVC Project. -2. Add the GraphQL ASP.NET Nuget Package. -3. Write some code to create a `Person` model object and a `PersonController` to deliver it. -4. Register GraphQL ASP.NET in `Startup.cs`. - -This guide uses [Visual Studio 2019](https://visualstudio.microsoft.com/) but the steps are similar for other IDEs, including [JetBrains Rider](https://www.jetbrains.com/rider/). - -The goal is to be able to open any GraphQL query tool such as [GraphiQL](https://electronjs.org/apps/graphiql), [Altair](https://altair.sirmuel.design/) or [GraphQL Playground](https://github.com/graphql/graphql-playground), point it at our server and execute this query: - -```javascript -query { - person(id: 18) { - id - firstName - lastName - forceUser - } -} -``` - -To generate this response: - -```javascript -{ - "data": { - "person": { - "id": 18, - "firstName": "Luke", - "lastName": "Skywalker", - "forceUser": true - } - } -} -``` - ---- - -## Step 1: Create a new ASP.NET MVC Project - -From the Visual Studio 2019 start screen: - -1. Choose `Create new Project` -2. Select `ASP.NET Core Web Application`. - - Enter your project's name and choose a location. -3. Choose `ASP.NET Core Web API` when prompted to select a project type. - - GraphQL ASP.NET has no view layer so we can forgo including Razor and other related options. - -![Create an API Project](../assets/quick-start-1-choose-api.png) - -## Step 2: Add the GraphQL ASP.NET Nuget Package - -1. Open the package manager `View -> Other Windows -> Package Manager Console` -2. Enter the command `Install-Package GraphQL.AspNet -AllowPrereleaseVersions` - - GraphQL ASP.NET has no external dependencies other than standard Microsoft packages. Follow the prompts to allow these packages to be installed for the project. -> Be sure to include the `-AllowPrereleaseVersions` flag. GraphQL ASP.NET is still in beta. - -![Create an API Project](../assets/quick-start-2-package-manager.png) - -## Step 3: Write Some Code - -### Person.cs - -Create a new class file called `Person.cs` and paste the following code. - -```cs -// Create a new "Person" model object -namespace GraphQLDemo -{ - public class Person - { - public int Id{ get; set; } - public string FirstName{ get; set; } - public string LastName { get; set; } - public bool ForceUser { get; set; } - public string FavoriteSong { get; set; } - } -} -``` - -The properties your model defines are automatically registered as fields on the object graph and can be queried in any order when a `Person` requested. - -### PersonsController.cs - -Add a new class file called `PersonController.cs` and paste the following code. - -```csharp -// Create a new GraphController to handle the request -using GraphQL.AspNet.Attributes; -using GraphQL.AspNet.Controllers; -namespace GraphQLDemo -{ - public class PersonsController : GraphController - { - [QueryRoot("person")] - public Person RetrievePerson(int id) - { - // Normally you'd do a database lookup here - return new Person() - { - Id = id, - FirstName = "Luke", - LastName = "Skywalker", - ForceUser = true, - FavoriteSong = "Papa was a Rollin' Stone", - }; - } - } -} -``` - -In an MVC controller, we'd generate a data object and pass it off to our Razor View for rendering. In GraphQL, we return the object back to GraphQL and let it handle which properties to render to the requestor based on their query. - -For a REST endpoint we'd use `[Route("person")]`, `[HttpGet("person")]` or similar attributes to specify the url template and HTTP verb for our action. In GraphQL, we have to specify if the action is a `[Query]` or a `[Mutation]` operation. Here we've chosen to use the special`[QueryRoot]` attribute to indicate that the action is both a query and exists at the top most level of our object graph. See the section on [declaring field paths](../controllers/field-paths) for a complete set of options and recommendations. - -## Step 4: Startup.cs - -1. Add a call to `AddGraphQL()` in in the ConfigureServices method. -2. Add a call to `UseGraphQL()` in the Configure method. - -We need to register GraphQL to the application instance. This registers it for dependency injection and sets up the HTTP route to allow it to receive requests. - -```csharp -// Startup.cs -using GraphQL.AspNet.Configuration.Mvc; -namespace GraphQLDemo -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services){ - - // ...other code omitted for brevity - services.AddGraphQL(); - } - - public void Configure(IApplicationBuilder application){ - - // ...other code hidden for brevity - application.UseGraphQL(); - } - } -} -``` - -_**Note:**_ Calling `.UseGraphQL()` will register the graphql handler for your schema into the ASP.NET http pipeline. Be sure to call it at an appropriate point. For instance, if you need authorization support in GraphQL, `.UseGraphQL()` will need to be called after `.UseAuthorization()`. - -## Step 5: Execute the Query - -Open your graphql tool of choice, point it at `http://localhost:5000/graphql` and execute the query: - -```javascript -query { - person(id: 18) { - id - firstName - lastName - forceUser - } -} -``` - -Here we used Altair to generate the result. - -![Altair Results](../assets/quick-start-5-altair-results.png) - -That's all there is. We've even pulled down the introspected documentation on the right. - -#### Try a few other things: - -- Alter your query to include `favoriteSong` and notice how the data package now includes that field without any C# changes. -- Misspell a field name. Not only does the tooling pick up the error client side but if you submit the query the JSON response from the server will indicate where the error occurred. -- Change the return type to `Task`, GraphQL automatically adjusts to execute asynchronously. -- If you are running the app on the console, make some adjustments to the default logging settings and add an entry for `"GraphQL.AspNet" : "Trace"` to see the log events write their output to the console window as your query executes. diff --git a/docs/reference/attributes.md b/docs/reference/attributes.md index 29a24b3..842b171 100644 --- a/docs/reference/attributes.md +++ b/docs/reference/attributes.md @@ -16,10 +16,12 @@ Declares that a given type system directive should be applied to the target sche public class Person { // apply by registered system type + // highlight-next-line [ApplyDirective(typeof(DeprecatedDirective))] public string FirstName{ get; set; } // apply by name, also with a reason parameter + // highlight-next-line [ApplyDirective("deprecated", "Last Name is deprecated")] public string LastName{ get; set; } } @@ -40,6 +42,7 @@ Declares a batch type extension with the given field name. The return type of th ```csharp public class HeroController : GraphController { + // highlight-next-line [BatchTypeExtension(typeof(Human), "droids")] public IDictionary> Hero(IEnumerable humans) { @@ -60,6 +63,7 @@ a batch collection result. ```csharp public class HeroController : GraphController { + // highlight-next-line [BatchTypeExtension(typeof(Human), "droids", typeof(IEnumerable))] public IGraphActionResult Hero(IEnumerable humans) { @@ -82,6 +86,7 @@ Declares the batch type extension as returning a union rather than a single spec ```csharp public class HeroController : GraphController { + // highlight-next-line [BatchTypeExtension(typeof(Human), "bestFriend", "DroidOrHuman", typeof(Droid), typeof(Human))] public IGraphActionResult Hero(IEnumerable humans) { @@ -104,6 +109,7 @@ Indicates to any introspection queries that the field or action method is deprec public class CharacterController : GraphController { [Query] + // highlight-next-line [Deprecated("Use the field SuperHero, this field will be removed soon")] public IGraphActionResult Hero(Episode episode = Episode.EMPIRE) { @@ -125,6 +131,7 @@ Adds a human-readable description to any type, interface, field, parameter, enum public class CharacterController : GraphController { [Query] + // highlight-next-line [Description("The hero of a given Star Wars Episode (Default: EMPIRE)")] public IGraphActionResult Hero(Episode episode = Episode.EMPIRE) { @@ -143,28 +150,8 @@ method should be invoked for a particular location. ```csharp public sealed class AllowFragment : GraphDirective { - [DirectiveLocations(ExecutableDirectiveLocation.FRAGMENT_SPREAD | ExecutableDirectiveLocation.INLINE_FRAGMENT)] - public IGraphActionResult Execute([FromGraphQL("if")] bool ifArgument) - { - return ifArgument ? this.Ok() : this.Cancel(); - } -} -``` - -## DirectiveInvocationPhase - -A seldom used attribute to instruct the runtime as to when the directive should be invoked. By default all directives are set to be executable -during `SchemaGeneration` and `AfterFieldResolution` depending on the allowed target locations. - -#### `[DirectiveInvocationPhase(phases)]` - -- `phases` - A bitwise set of `DirectiveInvocationPhase` values indicating when in the execution pipelines this directive should be invoked. - -```csharp -[DirectiveInvocationPhase(DirectiveInvocationPhase.AfterFieldResolution)] -public sealed class AllowFragment : GraphDirective -{ - [DirectiveLocations(ExecutableDirectiveLocation.FIELD)] + // highlight-next-line + [DirectiveLocations(DirectiveLocation.FRAGMENT_SPREAD | DirectiveLocation.INLINE_FRAGMENT)] public IGraphActionResult Execute([FromGraphQL("if")] bool ifArgument) { return ifArgument ? this.Ok() : this.Cancel(); @@ -185,6 +172,7 @@ and directive action methods. public class CharacterController : GraphController { [Query] + // highlight-next-line public IGraphActionResult Hero([FromGraphQL("id")] int heroId) { //.... @@ -200,8 +188,8 @@ public class CharacterController : GraphController public class CharacterController : GraphController { [Query] - public IGraphActionResult Hero( - [FromGraphQL(TypeExpression = "Type!")] string heroId) + // highlight-next-line + public IGraphActionResult Hero([FromGraphQL(TypeExpression = "Type!")] string heroId) { //.... } @@ -224,6 +212,7 @@ public enum Episode NewHope, Empire, + // highlight-next-line [GraphEnumValue("Jedi")] ReturnOfTheJedi, } @@ -243,6 +232,7 @@ public class Human { public int Id{get; set; } + // highlight-next-line [GraphField("name")] public string FullName { get; set; } } @@ -256,6 +246,7 @@ public class Human { public int Id{get; set; } + // highlight-next-line [GraphField(TypeExpression = "Type!")] public Employer Boss { get; set; } } @@ -271,6 +262,7 @@ Indicates that the controller should not attempt to register a virtual field for
```csharp +// highlight-next-line [GraphRoot] public class HeroController : GraphController { @@ -308,6 +300,7 @@ Indicates a field path in each root graph type where this controller should appe - The `"[controller]"` meta tag can be used and will be replaced by the controller name at runtime. ```csharp +// highlight-next-line [GraphRoute("starWars/characters")] public class HeroController : GraphController { @@ -337,15 +330,23 @@ Indicates that the entity to which its attached should be skipped and not includ #### `[GraphSkip]` -```csharp -public class CharacterController : GraphController +```csharp title="C# Class with GraphSkip" +public class Donut { - [Query] + public int Id{get; set;} + public string Name{get;set;} + + // highlight-next-line [GraphSkip] - public IGraphActionResult Hero([FromGraphQL("id")] int heroId) - { - // this method will not be included in the graph - } + public string Recipe {get; set;} +} +``` + +```graphql title="GraphQL Type Definition" +# +type Donut { + Id: String + Name: String } ``` @@ -353,16 +354,17 @@ public class CharacterController : GraphController Indicates additional or non-standard settings for the the class, interface or enum to which its attached. Also indicates the item is explicitly declared as a graph type and should be included in a schema. -#### [GraphType(name)] +#### `[GraphType(name)]` - `name` : The name of graph type as it should appear in the object graph -#### [GraphType(name, inputName)] +#### `[GraphType(name, inputName)]` - `name` : The name of graph type as it should appear in the schema when used as an `OBJECT` - `inputName`: The name of the graph type in the schema when used as an `INPUT_OBJECT` ```csharp +// highlight-next-line [GraphType("person", "personModel")] public class Human { @@ -375,15 +377,18 @@ public class Human Controller action method attributes that indicate the method belongs to the specified operation type (query or mutation). When declared as "Root" (i.e. `QueryRoot`), it indicates that the action method should be declared directly on its operation graph type and not nested underneath a controller's virtual field. -> All 4 action method attributes have identical constructor options +:::tip +`[Query]`, `[QueryRoot]`, `[Mutation]` and `[MutationRoot]` all have identical constructor options. +::: -#### [Query(template)] +#### `[Query(template)]` - `template` - The field path template to use for this method. ```csharp public class CharacterController : GraphController { + // highlight-next-line [Query("hero")] public Human RetrieveTheHero(Episode episode) { @@ -392,7 +397,7 @@ public class CharacterController : GraphController } ``` -#### [Query(returnType, params otherTypes)] +#### `[Query(returnType, params otherTypes)]` - `returnType`: the expected return type of this field. - must be used when this field returns an `IGraphActionResult` @@ -402,6 +407,7 @@ public class CharacterController : GraphController ```csharp public class CharacterController : GraphController { + // highlight-next-line [Query(typeof(Droid), typeof(Human))] public ICharacter Hero(Episode episode) { @@ -410,7 +416,7 @@ public class CharacterController : GraphController } ``` -#### [Query(template, returnType)] +#### `[Query(template, returnType)]` - `template` - The field path template to use for this method. - `returnType`: the expected return type of this field. @@ -419,6 +425,7 @@ public class CharacterController : GraphController ```csharp public class CharacterController : GraphController { + // highlight-next-line [Query("hero", typeof(Human))] public IGraphActionResult RetrieveTheHero(Episode episode) { @@ -438,6 +445,7 @@ public class CharacterController : GraphController ```csharp public class CharacterController : GraphController { + // highlight-next-line [Query("hero", "DroidOrHuman", typeof(Droid), typeof(Human))] public IGraphActionResult RetrieveCharacter(int id) { @@ -454,6 +462,7 @@ public class CharacterController : GraphController public class CharacterController : GraphController { // declare that this field must return a value (a null human is not allowed) + // highlight-next-line [Query("hero", typeof(Human), TypeExpression = "Type!")] public IGraphActionResult RetrieveTheHero(Episode episode) { @@ -476,6 +485,7 @@ Declares a type extension with the given field name. The return type of this fie ```csharp public class DroidController : GraphController { + // highlight-next-line [TypeExtension(typeof(Droid), "ownedBy")] public Human RetrieveDroidOwner(Droid droid) { @@ -495,6 +505,7 @@ Declares a type extension with an explicit return type. useful when returning `I ```csharp public class HeroController : GraphController { + // highlight-next-line [TypeExtension(typeof(Human), "ownedBy", typeof(Droid))] public IGraphActionResult RetrieveDroidOwner(Droid droid) { @@ -517,6 +528,7 @@ Declares the type extension as returning a union rather than a specific data typ ```csharp public class HeroController : GraphController { + // highlight-next-line [TypeExtension(typeof(Droid), "bestFriend", "DroidOrHuman", typeof(Droid), typeof(Human))] public IGraphActionResult RetrieveDroidsBestFriend(Droid droid) { diff --git a/docs/reference/performance.md b/docs/reference/benchmarks.md similarity index 100% rename from docs/reference/performance.md rename to docs/reference/benchmarks.md diff --git a/docs/reference/demo-projects.md b/docs/reference/demo-projects.md index 47cf340..dcf0254 100644 --- a/docs/reference/demo-projects.md +++ b/docs/reference/demo-projects.md @@ -5,31 +5,30 @@ sidebar_label: Demo Projects sidebar_position: 9 --- -📌 [Logging Provider](https://github.com/graphql-aspnet/demo-projects/tree/master/LoggingProvider) +### General -Demonstrates use of the structured event log by registering a `ILogProvider` and writing the log events to a json file. +📌 [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. -📌 [Custom Http Processor](https://github.com/graphql-aspnet/demo-projects/tree/master/Custom-HttpProcessor) +📌 [Logging Provider](https://github.com/graphql-aspnet/demo-projects/tree/master/LoggingProvider)
+Demonstrates the creation of a custom `ILogProvider` to intercept logging events and writing them to a json file. -Demonstrates overriding the default HTTP Processor to conditionally process or reject entire queries. +📌 [Custom Http Processor](https://github.com/graphql-aspnet/demo-projects/tree/master/Custom-HttpProcessor)
+Demonstrates overriding the default HTTP Processor to conditionally process entire queries at the ASP.NET level. -📌 [Field Authorization](https://github.com/graphql-aspnet/demo-projects/tree/master/Authorization) +### Authentication & Authorization +📌 [Field Authorization](https://github.com/graphql-aspnet/demo-projects/tree/master/Authorization)
Demonstrates fields with authorization requirements and how access denied messages are returned to the client in the various authorization modes. -📌 [Firebase Authentication](https://github.com/graphql-aspnet/demo-projects/tree/master/Firebase-Authentication) - +📌 [Firebase Authentication](https://github.com/graphql-aspnet/demo-projects/tree/master/Firebase-Authentication)
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 -📌 [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. - -📌 [Subscriptions w/ React & Apollo Client](https://github.com/graphql-aspnet/demo-projects/tree/master/Subscriptions-ReactApolloClient) +📌 [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. +📌 [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/reference/global-configuration.md b/docs/reference/global-configuration.md index f9a90a2..93e9b1f 100644 --- a/docs/reference/global-configuration.md +++ b/docs/reference/global-configuration.md @@ -13,10 +13,6 @@ Global configuration settings affect the entire server instance, they are not re // ------------------- services.AddGraphQL(); - - -// Be sure to add graphql to the ASP.NET pipeline builder -appBuilder.UseGraphQL(); ``` ## General @@ -26,14 +22,14 @@ The configured service lifetime that all discovered controllers and directives w process. ```csharp -GraphQLProviders.GlobalConfiguration.ControllerServiceLifetime = ServiceLifetime.Transient; +GraphQLServerSettings.ControllerServiceLifetime = ServiceLifetime.Transient; ``` | Default Value | Acceptable Values | | ------------- | ----------------- | | `Transient ` | `Transient`, `Scoped`, `Singleton` | :::danger - 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. + Registering GraphControllers as anything other than transient can cause unexpected behavior and result in unexplained crashes, data loss, data exposure and security issues in some scenarios. Consider restructuring your application before changing this setting. Adjusting this value should be a last resort, not a first option. ::: ## Subscriptions @@ -43,7 +39,7 @@ GraphQLProviders.GlobalConfiguration.ControllerServiceLifetime = ServiceLifetime 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 = 500; +GraphQLSubscriptionServerSettings.MaxConcurrentReceiverCount = 500; ``` | Default Value | Acceptable Values | @@ -57,11 +53,11 @@ Indicates the maximum number of client connections this server instance will acc ```csharp -SubscriptionServerSettings.MaxConnectedClientCount = null; +GraphQLSubscriptionServerSettings.MaxConnectedClientCount = null; ``` | Default Value | Acceptable Values | | ------------- | ----------------- | -| `null` | null OR > 0 | +| _-not set-_ | null OR > 0 | -_Note:_ `null` _indicates that no limits will be enforced._ \ No newline at end of file +> Note: By default this value is not set, indicating there is no limit. GraphQL will accept any connection passed by the ASP.NET runtime. \ No newline at end of file diff --git a/docs/reference/how-it-works.md b/docs/reference/how-it-works.md index d3bb0dd..c117fb4 100644 --- a/docs/reference/how-it-works.md +++ b/docs/reference/how-it-works.md @@ -5,7 +5,7 @@ sidebar_label: How it Works sidebar_position: 0 --- -> This document is a high level overview how GraphQL ASP.NET ultimately generates a response to a query with some insight into core details. Its assumes a working knowledge of both ASP.NET and the GraphQL specification. If you are only interested in the "how to" for using the library, feel free to skip this. +> This document is a high level overview how GraphQL ASP.NET ultimately generates a response to a query with some insight into core details. Its assumes a working knowledge of both ASP.NET and the GraphQL specification. If you are only interested in the "how" and not the "why", feel free to skip this. ## Schema Generation diff --git a/docs/reference/http-processor.md b/docs/reference/http-processor.md index 83714a7..d4c0180 100644 --- a/docs/reference/http-processor.md +++ b/docs/reference/http-processor.md @@ -5,7 +5,7 @@ sidebar_label: HTTP Processor sidebar_position: 6 --- -The `DefaultGraphQLHttpProcessor` is mapped to a route for the target schema and accepts an `HttpContext` from the ASP.NET runtime. It inspects the received payload (the query text and variables) then packages an `IGraphOperationRequest` and sends it to the GraphQL runtime. Once a result is generated the controller forwards that response to the response writer for serialization. +The `DefaultGraphQLHttpProcessor` is mapped to a route for the target schema and accepts an `HttpContext` from the ASP.NET runtime. It inspects the received payload (the query text and variables) then packages an `IQueryExecutionRequest` and sends it to the GraphQL runtime. Once a result is generated the controller forwards that response to the response writer for serialization. ## Extending the Http Processor @@ -18,9 +18,8 @@ public class MyHttpProcessor : DefaultGraphQLHttpProcessor { public MyHttpProcessor( MySchema schema, - ISchemaPipeline queryPipeline, - IGraphResponseWriter writer, - IGraphQueryExecutionMetricsFactory metricsFactory, + IGraphQLRuntime queryPipeline, + IQueryResponseWriter writer, IGraphEventLogger logger = null) : base(schema, queryPipeline, writer, metricsFactory, logger) { @@ -45,13 +44,13 @@ These methods can be overridden to provide custom logic at various points in the ### CreateRequest(queryData) -Override this method to supply your own `IGraphOperationRequest` to the runtime. +Override this method to supply your own `IQueryExecutionRequest` to the runtime. - `queryData`: The raw data package read from the HttpContext ### HandleQueryException(exception) -Override this method to provide some custom processing to an unhandled exception. If this method returns an `IGraphOperationResult` it will be sent to the requestor, otherwise return null to allow a status 500 result to be generated. +Override this method to provide some custom processing to an unhandled exception. If this method returns an `IQueryExecutionResult` it will be sent to the requestor, otherwise return null to allow a status 500 result to be generated. It is exceedingly rare that this method will ever be called. The runtime will normally attach exceptions as messages to the graphql response. This method exists as a catch all _just in case_ something occurs beyond all expected constraints. @@ -61,7 +60,7 @@ It is exceedingly rare that this method will ever be called. The runtime will no Override this method to perform some custom processing on a set of query metrics that were gathered for the executed query. This method will only be called if metrics were actually gathered. -- `metrics`: the `IGraphQueryExecutionMetrics` package that was populated during the request. +- `metrics`: the `IQueryExecutionMetrics` package that was populated during the request. ### ExposeExceptions diff --git a/docs/reference/middleware.md b/docs/reference/middleware.md index 05d3adc..64dc879 100644 --- a/docs/reference/middleware.md +++ b/docs/reference/middleware.md @@ -29,7 +29,7 @@ The interfaces define one method, `InvokeAsync`, with identical signatures save public interface IQueryExecutionMiddleware { Task InvokeAsync( - GraphQueryExecutionContext context, + QueryExecutionContext context, GraphMiddlewareInvocationDelegate next, CancellationToken cancelToken); } @@ -41,7 +41,7 @@ The library will invoke your component at the appropriate time and pass to it th public class MyQueryMiddleware : IQueryExecutionMiddleware { public async Task InvokeAsync( - GraphQueryExecutionContext context, + QueryExecutionContext context, GraphMiddlewareInvocationDelegate next, CancellationToken cancelToken) { @@ -85,7 +85,7 @@ Instead of adding to the end of the existing pipeline you can also call `.Clear( 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. +- `QueryRequest`: 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. @@ -110,33 +110,33 @@ The query execution pipeline is invoked once per request. It is supplied with th 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. `QueryExecutionPlanCacheMiddleware` : 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. `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. +7. `ValidateOperationVariableDataMiddleware`: 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. +12. `PackageQueryResultMiddleware`: Performs a final set of checks on the resolved field data and generates an `IQueryExecutionResult` for the query. document on the context. -#### GraphQueryExecutionContext +#### QueryExecutionContext In addition to the common properties defined above, the query execution context defines a number of useful fields: -```csharp title="GraphQueryExecutionContext.cs" -public class GraphQueryExecutionContext +```csharp title="QueryExecutionContext.cs" +public class QueryExecutionContext { - public IGraphOperationResult Result { get; set; } - public IGraphQueryPlan QueryPlan { get; set; } - public IList FieldResults { get; } + public IQueryExecutionResult Result { get; set; } + public IQueryExecutionPlan QueryPlan { get; set; } + public IList FieldResults { get; } // other properties omitted for brevity } ``` -- `Result`: The created `IGraphOperationResult`. This property will be null until the result is created. +- `Result`: The created `IQueryExecutionResult`. 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. diff --git a/docs/reference/query-caching.md b/docs/reference/query-caching.md index 9569644..6a17917 100644 --- a/docs/reference/query-caching.md +++ b/docs/reference/query-caching.md @@ -21,7 +21,8 @@ At startup, inject the query cache into the service collection. The cache itself ```csharp title="Startup Code" // Register the query cache BEFORE calling .AddGraphQL -service.AddGraphQLLocalQueryCache(); +// highlight-next-line +services.AddGraphQLLocalQueryCache(); services.AddGraphQL(); ``` diff --git a/docs/reference/schema-configuration.md b/docs/reference/schema-configuration.md index 15165b6..a84faec 100644 --- a/docs/reference/schema-configuration.md +++ b/docs/reference/schema-configuration.md @@ -57,12 +57,14 @@ Adds a single entity of a given type the schema. Use these methods to add indivi ### ApplyDirective ```csharp -schemaOptions.ApplyDirective("deprecated") +schemaOptions.ApplyDirective("@deprecated") .WithArguments("The name field is deprecated.") .ToItems(schemaItem => schemaItem.IsGraphField("name")); ``` -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. +Allows for the runtime registration of a type system directive to a given schema item. + +>See the section on [directives](../advanced/directives.md#using-schema-options) for complete details on how to use this method. ### AutoRegisterLocalEntities ```csharp @@ -74,7 +76,7 @@ schemaOptions.AutoRegisterLocalEntities = true; | ------------- | ----------------- | | `true` | `true`, `false` | -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. +When true, the graph entities (controllers, types, enums etc.) that are declared in the startup 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 @@ -111,7 +113,7 @@ schemaOptions.DeclarationOptions.AllowedOperations.Remove(GraphOperationType.Mut 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._ +> Subscriptions are automatically added when the subscription library is added via `.AddSubscriptions()`. ### DisableIntrospection @@ -124,9 +126,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 will fail, preventing exposure of type meta data. -_Note: Many tools, IDEs and client libraries will fail if you disable introspection data._ +> Note: Many tools, IDEs and client libraries not work if you disable introspection data. ### FieldDeclarationRequirements ```csharp @@ -142,11 +144,11 @@ Indicates to the runtime which fields and values of POCO classes must be explici By default: -- 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 values declared on an `enum` **will be** included. +- All properties of POCOs and interfaces **will be** included. +- All methods of POCOs and interfaces **will NOT be** included. -_NOTE: Controller and Directive action methods are not effected by this setting. Any_ `[GraphField]` _declaration will automatically override these settings._ +> NOTE: Controller and Directive action methods are not effected by this setting. ### GraphNamingFormatter @@ -155,7 +157,7 @@ _NOTE: Controller and Directive action methods are not effected by this setting. schemaOptions.DeclarationOptions.GraphNamingFormatter = new GraphNameFormatter(...); ``` -An object that will format any internal name of a class or method to an acceptable name for use in the object graph. +An object that will format any string to an acceptable name for use in the graph. | Entity Type | Default Format | Examples | | ---------------- | -------------- | ------------------------------------ | @@ -194,7 +196,7 @@ schemaOptions.ExecutionOptions.EnableMetrics = false; When true, metrics and query profiling will be enabled for all queries processed for a given schema. -_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. +> Note: This option DOES NOT control if those metrics are sent to the query requestor, just that they are generated. See [ExposeMetrics](./schema-configuration#exposemetrics) in the response options for that switch. ### MaxQueryComplexity @@ -245,7 +247,7 @@ schemaOptions.ExecutionOptions.ResolverIsolation = ResolverIsolationOptions.Cont | Default Value | | ------------- | -| `ResolverIsolation.None` | +| `ResolverIsolationOptions.None` | 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. @@ -278,7 +280,9 @@ schemaOptions.ResponseOptions.ExposeExceptions = false; 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. +:::caution WARNING +Setting this value to true can expose sensitive server details and may be considered a security risk. +::: ### ExposeMetrics @@ -291,15 +295,15 @@ schemaOptions.ResponseOptions.ExposeMetrics = false; | ------------- | ----------------- | | `false` | `true`, `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. +When true, the full set of metrics gathered when a query is executed is sent to the requestor. This value is disregarded unless `ExecutionOptions.EnableMetrics` is set to true. -_Note: Metrics data for large queries can be quite expansive; double or tripling the size of the json data returned._ +> 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.ExposeExceptions = true; +schemaOptions.ResponseOptions.IndentDocument = true; ``` | Default Value | Acceptable Values | @@ -312,13 +316,19 @@ When true, the default json response writer will indent and "pretty up" the outp ```csharp // usage examples -schemaOptions.ResponseOptions.AppendServerHeader = GraphMessageSeverity.Information; +schemaOptions.ResponseOptions.MessageSeverityLevel = GraphMessageSeverity.Information; ``` | Default Value | Acceptable Values | | ---------------------------------- | -------------------------------------- | -| `GraphMessageSeverity.Information` | \-_any `GraphMessageSeverity` value_\- | +| `Information` | \-_any `GraphMessageSeverity` value_\- | + +Indicates which messages generated during a query should be sent to the requestor. Any message with a [severity level](https://github.com/graphql-aspnet/graphql-aspnet/blob/master/src/graphql-aspnet/Execution/GraphMessageSeverity.cs) equal to or greater than the provided level will be delivered. + +#### Message Severity Levels -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. +|Value | Rank | +|--------|--------| +| ### TimeStampLocalizer @@ -327,11 +337,11 @@ Indicates which messages generated during a query should be sent to the requesto schemaOptions.ResponseOptions.TimeStampLocalizer = (dtos) => dtos.DateTime; ``` -| Func | -| --------------------------------- | -| `(dtoffset) => dtoffset.DateTime` | +|Default Value | Acceptable Value | +| -------------|--------------------------------- | +|_`null`_ | `Func` | -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. +A function to convert any system-provided timestamp values 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 (e.g. message creation dates, start and stop times for query metrics etc.) are effected. ## QueryHandler Options @@ -346,7 +356,11 @@ schemaOptions.QueryHandler.AuthenticatedRequestsOnly = false; | ------------- | ----------------- | | `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 setting has no effect when a custom `HttpProcessorType` is declared. +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. + +:::note + This setting acts as a short cut to assigning custom HttpProcessorType. If you provide your own custom [`HttpProcessorType`](#httpprocessortype) this setting has no effect. +::: ### DisableDefaultRoute @@ -374,7 +388,11 @@ 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 `System.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`. + +:::tip +It can be easier to extend `DefaultGraphQLHttpProcessor` instead of implementing the interface from scratch if you only need to make minor changes. +::: ### Route @@ -390,14 +408,28 @@ schemaOptions.QueryHandler.Route = "/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)` +These options are available to configure a subscription server for a given schema via `.AddSubscriptions(subscriptionOptions)` + +```csharp title="Adding Subscription Configuration Options" +services.AddGraphQL() + .AddSubscriptions(subscriptionOptions => + { + // ************************* + // CONFIGURE YOUR SUBSCRIPTION + // OPTIONS HERE + // ************************* + }); +// Be sure to add graphql to the ASP.NET pipeline builder +appBuilder.UseGraphQL(); +``` + ### AuthenticatedRequestsOnly ```csharp // usage examples -serverOptions.AuthenticatedRequestsOnly = false; +subscriptionOptions.AuthenticatedRequestsOnly = false; ``` | Default Value | Acceptable Values | @@ -412,14 +444,16 @@ The interval at which the subscription server will send a protocol-specific mess ```csharp // usage examples -serverOptions.ConnectionKeepAliveInterval = TimeSpan.FromMinutes(2); +subscriptionOptions.ConnectionKeepAliveInterval = TimeSpan.FromMinutes(2); ``` | Default Value | | ------------- | | `2 minutes` | -_Note: Not all messaging protocols support message level keep alives._ +:::tip +This is an application level keep-alive supported by most graphql messaging protocols. This is a different keep-alive than the web socket specific keep alive provided by ASP.NET +::: ### ConnectionInitializationTimeout @@ -428,7 +462,7 @@ When supported by a messaging protocol, represents a timeframe after the connect ```csharp // usage examples -serverOptions.ConnectionInitializationTimeout = TimeSpan.FromSeconds(30); +subscriptionOptions.ConnectionInitializationTimeout = TimeSpan.FromSeconds(30); ``` | Default Value | @@ -436,7 +470,7 @@ serverOptions.ConnectionInitializationTimeout = TimeSpan.FromSeconds(30); | `30 seconds` | -_Note: Not all messaging protocols require an explicit timeframe or support an inititalization handshake._ +> Note: Not all messaging protocols require an explicit timeframe or support an inititalization handshake. ### DefaultMessageProtocol @@ -444,26 +478,26 @@ When set, represents a valid and supported messaging protocol that a client shou ```csharp // usage examples -serverOptions.DefaultMessageProtocol = "my-custom-protocol"; +subscriptionOptions.DefaultMessageProtocol = "my-custom-protocol"; ``` | Default Value | | ------------- | | `null` | -_Note: By default, this value is not set and connected clients MUST supply a prioritized protocol list._ +> Note: By default, this value is not set and connected clients **MUST** supply a prioritized protocol list. ### DisableDefaultRoute ```csharp // usage examples -serverOptions.DisableDefaultRoute = false; +subscriptionOptions.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<TSchema>](https://github.com/graphql-aspnet/graphql-aspnet/blob/master/src/graphql-aspnet-subscriptions/Defaults/DefaultGraphQLHttpSubscriptionMiddleware.cs) may help. +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<TSchema>](https://github.com/graphql-aspnet/graphql-aspnet/blob/master/src/graphql-aspnet-subscriptions/Engine/DefaultGraphQLHttpSubscriptionMiddleware.cs) may help. @@ -473,7 +507,7 @@ When set, represents the custom middleware component GraphQL will inject into th ```csharp // usage examples -serverOptions.HttpMiddlewareComponentType = typeof(MyMiddleware); +subscriptionOptions.HttpMiddlewareComponentType = typeof(MyMiddleware); ``` | Default Value | @@ -488,7 +522,7 @@ Deteremines if a web socket request will be accepted in an unauthenticated state ```csharp // usage examples -serverOptions.RequiredAuthenticatedConnection = false; +subscriptionOptions.RequiredAuthenticatedConnection = false; ``` | Default Value | Acceptable Values | @@ -506,7 +540,7 @@ Similar to the query/mutation query handler route this represents the path the d ```csharp // usage examples -serverOptions.Route = "/graphql"; +subscriptionOptions.Route = "/graphql"; ``` | Default Value | @@ -516,6 +550,10 @@ serverOptions.Route = "/graphql"; 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. +:::info +Your subscriptions can share the same route as your general queries for a schema or be different, its up to you. +::: + ### SupportedMessageProtocols 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 @@ -533,4 +571,4 @@ serverOptions.SupportedMessageProtocols = myProtocols; | ------------- | | `null` | -_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._ +> By default, `SupportedMessageProtocols` 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/reference/vocabulary.md b/docs/reference/vocabulary.md new file mode 100644 index 0000000..b276047 --- /dev/null +++ b/docs/reference/vocabulary.md @@ -0,0 +1,30 @@ +--- +id: vocabulary +title: Vocabulary +sidebar_label: Vocabulary +sidebar_position: 11 +--- + +### Fields & Resolvers +In GraphQL terms, a field is any requested piece of data (such as an id or name). A resolver fulfills the request for data from a schema field. It takes in a set of input arguments and produces a piece of data that is returned to the client. In GraphQL ASP.NET your controller methods act as resolvers for top level fields in any query. + +### Graph Type +A graph type is an entity on your object graph; a droid, a donut, a string, a number etc. In GraphQL ASP.NET your model classes, interfaces, enums, controllers etc. are compiled into the various graph types required by the runtime. + +### Query Document +This is the raw query text submitted by a client. When GraphQL accepts a query it is converted from a string to an internal document format that is parsed and used to fulfill the request. + +> Queries, Mutations and Subscriptions are all types of query documents. + +### Root Graph Types +There are three root graph types in GraphQL: Query, Mutation, Subscription. Whenever you make a graphql request, you always specify which query root you are targeting. This documentation will usually refer to all operations as "queries" but this includes mutations and subscriptions as well. + + +### Schema +This is the set of public data types, their fields, input arguments etc. that are exposed on an object graph. When you write a graphql query to return data, the fields you request must all be defined on a schema that graphql will validate your query against. + +Your schema is "generated" at runtime by analyzing your model classes, controllers and action methods then populating a `GraphSchema` container with the appropriate graph types to map graphql requests to your controllers. + +:::note + In GraphQL ASP.NET the schema is generated at runtime directly from your C# controllers and POCOs; there is no additional boilerplate code necessary to define a schema. +::: \ No newline at end of file diff --git a/docs/types/enums.md b/docs/types/enums.md index 565dfaa..ede543f 100644 --- a/docs/types/enums.md +++ b/docs/types/enums.md @@ -61,7 +61,7 @@ enum DonutType { ## Excluding an Enum Value -Use the `[GraphSkip]` attribute to omit a value from the graph. A query will be rejected if it attempts to submit a valid, yet omitted, enum value. +Use the `[GraphSkip]` attribute to omit a value from the schema. A query will be rejected if it attempts to submit an omitted enum value. ```csharp title="DonutType.cs" diff --git a/docs/types/input-objects.md b/docs/types/input-objects.md index 7db717b..05baa8a 100644 --- a/docs/types/input-objects.md +++ b/docs/types/input-objects.md @@ -82,8 +82,8 @@ public class Donut ``` ```graphql title="Donut Type Definition" -// GraphQL Type Definition -// Id field is not included on the INPUT_OBJECT +# Id field is not included + input Input_Donut { name: String = null type: DonutType! = FROSTED @@ -100,8 +100,7 @@ While its possible to have methods be exposed as resolvable fields on regular `O public class Donut { [GraphField("salesTax")] - public decimal CalculateSalesTax( - decimal taxPercentage) + public decimal CalculateSalesTax(decimal taxPercentage) { return this.Price * taxPercentage; } @@ -123,6 +122,7 @@ input Input_Donut { } ``` + ## Required Fields And Default Values Add `[Required]` (from System.ComponentModel) to any property to force a user to supply the field in a query document. @@ -135,7 +135,7 @@ public class Donut public Donut() { // set custom defaults if needed - this.Type = DonutType.Vanilla; + this.Type = DonutType.Frosted; this.Price = 2.99; this.IsAvailable = true; } @@ -147,14 +147,14 @@ public class Donut public DonutType Type { get; set; } public Bakery Bakery { get;set; } public decimal Price { get; set; } - public decimal IsAvailable { get; set; } + public bool IsAvailable { get; set; } } ``` ```graphql title="Donut Type Definition" # No Default Value on Name input Input_Donut { - name: String! + name: String id: Int! = 0 type: DonutType! = FROSTED bakery: Input_Bakery = null @@ -167,50 +167,51 @@ input Input_Donut { ## Non-Nullability By default, all properties that are reference types (i.e. classes) are nullable and all value types (primatives, structs etc.) are non-nullable - - -```csharp title="Owner can be null, it is a reference type" -public class Bakery +```csharp title="Recipe can be null, it is a reference type" +public class Donut { - // a reference to another object - public Person Owner { get; set; } + public Recipe Recipe { get; set; } // reference type + public int Quantity { get; set; } // value type } ``` -```graphql title="Donut Type Definition" -input Input_Bakery { - owner: Input_Person = null +```graphql title="Input Donut Definition" +input Input_Donut { + recipe: Input_Recipe = null # nullable + quantity: Int! = 0 # not nullable } ``` -If you want to force a value to be supplied (either on a query document or by default) you can use the `[GraphField]` attribute to augment the field. +If you want to force a reference type to be non-null you can use the `[GraphField]` attribute to augment the field's type expression. - - - -```csharp title="Force Owner to be non-null" -public class Bakery +```csharp title="Force Recipe to be non-null" +public class Donut { - public Bakery() + public Donut() { - this.Owner = new Person("Bob Smith"); + this.Recipe = new Recipe("Flour, Sugar, Salt"); } - // a reference to another object [GraphField(TypeExpression = "Type!")] - public Person Owner { get; set; } + public Recipe Recipe { get; set; } // reference type + public int Quantity { get; set; } // value type } ``` -```graphql title="Donut Type Definition" -input Input_Bakery { - owner: Input_Person! = { name: "Bob Smith" } +```graphql title="Input Donut Definition" +input Input_Donut { + recipe: Input_Recipe! = {Ingredients : "Flour, Sugar, Salt" } + quantity: Int! = 0 } ``` -:::info - Any field explicitly or implicitly declared as non-nullable, that is not required, MUST have a default value assigned to it that is not `null`. A `GraphTypeDeclarationException` will be thrown at startup if this is not the case. +:::info Did You Notice? + We assigned a recipe in the class's constructor to use as the default value. + + Any non-nullable field, that does not have the `[Required]` attribute, MUST have a default value assigned to it that is not `null`. + + A `GraphTypeDeclarationException` will be thrown at startup if this is not the case. ::: #### Combine Non-Null and [Required] @@ -236,17 +237,16 @@ input Input_Bakery { } ``` -## Default Values Must be Coercible -Any default value declared for an input field must be coercible by its target graph type in the target schema. +## Enum Fields and Coercability -### Enum Values +Any default value declared for an input field must be coercible by its target graph type in the target schema. Because of this there is a small got'cha situation with enum values. Take a look at this example of an enum and input object: ```csharp title="Using an Enum as a field type" public class Donut { - public string Name{ get; set; } + public string Name { get; set; } public DonutFlavor Flavor { get; set; } } @@ -255,14 +255,32 @@ public enum DonutFlavor [GraphSkip] Vanilla = 0, Chocolate = 1, - } ``` When `Donut` is instantiated the value of Flavor will be `Vanilla` because thats the default value (0) of the enum. However, the enum value `Vanilla` is marked as being skipped in the schema. -Because of this mismatch, a `GraphTypeDeclarationException` will be thrown when the introspection data for your schema is built. As a result, the server will fail to start until the problem is corrected. +Because of this mismatch, a `GraphTypeDeclarationException` will be thrown when the introspection data for your schema is built. As a result, the server will fail to start until the problem is corrected. + +You can get around this by setting an included enum value in the consturctor: + + +```csharp title="Using an Enum as a field type" +public class Donut +{ + public Donut() + { + // set the value of flavor to an enum value + // included in the graph + this.Flavor = DonutFlavor.Chocolate; + } + + public string Name { get; set; } + public DonutFlavor Flavor { get; set; } +} + +``` :::caution Enum values used for the default value of input object properties MUST also exist as values in the schema or an exception will be thrown. diff --git a/docs/types/interfaces.md b/docs/types/interfaces.md index ddda86d..da4af12 100644 --- a/docs/types/interfaces.md +++ b/docs/types/interfaces.md @@ -5,9 +5,9 @@ sidebar_label: Interfaces sidebar_position: 2 --- -Interfaces in GraphQL work like interfaces in C#. They provide a contract for a set of common fields of different objects. When it comes to declaring them, the `INTERFACE` graph type works exactly like [object types](./objects). +Interfaces in GraphQL work like interfaces in C#. They provide a contract for a set of common fields amongst different objects. When it comes to declaring them, the `INTERFACE` graph type works exactly like [object types](./objects). -By Default, when creating an interface graph type GraphQL: +By default, when creating an interface graph type GraphQL: - Will name the interface the same as its C# type name. - Will include all properties that have a getter. @@ -32,13 +32,34 @@ interface IPastry { The section on working with interfaces with [action methods](../controllers/actions#working-with-interfaces) provides a great discussion on proper usage but its worth pointing out here as well. -:::info -If your schema contains an interface (`IPastry`) it must also contain the objects that implement it (`Cake` and `Donut`). -::: - You must let GraphQL know of the possible object types you intend to return as the interface. If your action method returns `IPastry` and you return a `Donut`, but didn't let GraphQL know about the `Donut` class, it won't be able to continue to resolve the requested fields. -Most of the time GraphQL is smart enough to figure out which object types you're referencing by looking at the complete scope of actions and objects in your schema and won't bug you about it. But the steps outlined with defining action methods describe ways to ensure you have no issues. +```csharp title="BakeryController.cs" +public class BakeryController : GraphController +{ + // highlight-next-line + [QueryRoot(typeof(Donut), typeof(Cake))] + public IPastry SearchPastries(string name) + {/* ... */} +} +``` + +```graphql title="Sample Query" +query { + searchPastries(name: "chocolate*") { + id + name + + ...on Donut { + isFilled + } + + ...on Cake { + icingFlavor + } + } +} +``` ## Use It To Include It @@ -63,8 +84,8 @@ public interface IPastry ```graphql title="Type Definitions" -// Donut is published on the schema -// IPastry is not included +# Donut is published on the schema +# but IPastry is not included type Donut { id: Int! name: String @@ -87,6 +108,7 @@ public class BakeryController : GraphController // ERROR! // A GraphTypeDeclarationException will be thrown [Mutation] + // highlight-next-line public Donut AddNewDonut(IPastry newPastry) {/* ... */} } @@ -103,6 +125,7 @@ Like with other graph types use the `[GraphType]` attribute to indicate a custom ```csharp title="Interface Custom Name" +// highlight-next-line [GraphType("Pastry")] public interface IPastry { @@ -144,6 +167,7 @@ This can create some less than ideal scenarios. For instance, if only `IDonut` i ```csharp title="Startup Code" services.AddGraphQL(o => { + // highlight-next-line o.AddGraphType(); }); ``` @@ -162,8 +186,10 @@ However GraphQL does support interface inheritance. As long as both interfaces a ```csharp title="Startup Code" services.AddGraphQL(o => { - o.AddGraphType(); + // highlight-start + o.AddGraphType(); o.AddGraphType(); + // highlight-end }); ``` diff --git a/docs/types/list-non-null.md b/docs/types/list-non-null.md index c8f732b..329bbc3 100644 --- a/docs/types/list-non-null.md +++ b/docs/types/list-non-null.md @@ -51,9 +51,9 @@ Type Expressions are commonly shown in the GraphQL schema syntax for field defin ### Overriding Type Expressions -You may need to override the default behavior from time to time. For instance, a `string`, which is a reference type, is nullable by default but you may need to require a string be supplied in an input argument. +You may need to override the default behavior from time to time. For instance, a `string`, which is a reference type, is nullable by default but you may need to declare that null is not a valid string. Or, perhaps, an object implements `IEnumerable` but you don't want graphql to treat it as a list. -You can override the default behavior by defining a [custom type expression](../advanced/type-expressions) when needed. +You can override the default type expression of any field or argument by defining a [custom type expression](../advanced/type-expressions) when needed. ## Runtime Type Validation diff --git a/docs/types/scalars.md b/docs/types/scalars.md index 6c01358..9bbbbca 100644 --- a/docs/types/scalars.md +++ b/docs/types/scalars.md @@ -79,3 +79,6 @@ string str = "abc"; GraphId id = (GraphId)str; // id.Value == "abc" ``` + +## Custom Scalars +See the section on [custom scalars](../advanced/custom-scalars.md) for details on creating your own scalar types. \ No newline at end of file diff --git a/docs/types/unions.md b/docs/types/unions.md index 62245d9..d0f2a4d 100644 --- a/docs/types/unions.md +++ b/docs/types/unions.md @@ -5,7 +5,7 @@ sidebar_label: Unions sidebar_position: 3 --- -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. +Unions are an aggregate graph type representing multiple, different `OBJECT` types with no guaranteed fields or interfaces in common; for instance, `Salad` or `House`. 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 semi-virtual types that are created from proxy classes that represent them at design time. @@ -13,27 +13,27 @@ Unlike other graph types there is no concrete representation of unions. Where a You can declare a union in your action method using one of the many overloads to the query and mutation attributes: - ```csharp title="Declaring a Union on an Action Method" -public class KitchenController : GraphController +public class DataController : GraphController { - [QueryRoot("searchFood", "SaladOrBread", typeof(Salad), typeof(Bread))] - public ????? RetrieveFood(string name) + // highlight-next-line + [QueryRoot("search", "SaladOrHouse", typeof(Salad), typeof(House))] + public ????? SearchData(string name) {/* ... */} } ``` ```graphql title="Example Query" query { - searchFood(name: "caesar*") { + search(name: "green*") { ...on Salad { name hasCroutons } - ...on Bread { - name - hasGarlic + ...on House { + postalCode + squareFeet } } } @@ -41,37 +41,38 @@ query { In this example we : -- 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` +- Declared an action method named `SearchData` with a graph field name of `search` +- Declared a union type on our graph named `SaladOrHouse` +- Included two object types in the union: `Salad` and `House` -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. - -> Union member types must be declared as part of the union. +:::tip +Unlike with [interfaces](./interfaces) where the possible types returned from an action method can be declared else where, you MUST provide all of the types to include in the union in the declaration. +::: ### 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, 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. +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 `IDataItem` interface shared between `Salad` and `House`. 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 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. ```csharp title="Return IGraphActionResult When Working With Unions" -public class KitchenController : GraphController +public class DataController : GraphController { // service injection omitted for brevity - [QueryRoot("searchFood", "SaladOrBread", typeof(Salad), typeof(Bread))] - public async Task SearchFood(string name) + [QueryRoot("search", "SaladOrHouse", typeof(Salad), typeof(House))] + // highlight-next-line + public async Task SearchData(string text) { if(name.Contains("green")) { - Salad salad = await _saladService.FindSalad(name); + Salad salad = await _saladService.FindSalad(text); return this.Ok(salad); } else { - Bread bread = await _breadService.FindBread(name); - return this.Ok(bread); + House house = await _houses.FindHouse(text); + return this.Ok(house); } } } @@ -81,32 +82,58 @@ public class KitchenController : GraphController Any controller action that declares a union MUST return an `IGraphActionResult` ::: +#### Returning a List of Objects +Perhaps the most complex scenario when working with unions is returning a list of objects. Since there there is no way to declare a `List` that the library could analyze we have to explicitly declare the field to let GraphQL what is going on. + + +```csharp title="Return a List of Objects" +public class DataController : GraphController +{ + // service injection omitted for brevity + // highlight-next-line + [QueryRoot("search", "SaladOrHouse", typeof(Salad), typeof(House), TypeExpression = "[Type]")] + public async Task SearchData(string text) + { + Salad salad = await _saladService.FindSalad(text); + House house = await _houses.FindHouse(text); + + var dataItems = new List(); + dataItems.Add(salad); + dataItems.Add(house); + + return this.Ok(dataItems); + } +} +``` +> Here we've added a custom type expression to tell GraphQL that this field returns a list of objects. GraphQL will then process each item on the enumeration according to the rules of the union. + ## Union Proxies -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. +In the example above, we declare the union inline on the query attribute. But what if we wanted to reuse the `SaladOrHouse` 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 title="Example Using IGraphUnionProxy" public class KitchenController : GraphController { - [QueryRoot("searchFood", typeof(SaladOrBread))] + // highlight-next-line + [QueryRoot("searchFood", typeof(SaladOrHouse))] public async Task SearchFood(string name) {/* ... */} } -// SaladOrBread.cs -public class SaladOrBread : GraphUnionProxy +// highlight-next-line +public class SaladOrHouse : GraphUnionProxy { - public SaladOrBread() + public SaladOrHouse() : base() { - this.Name = "SaladOrBread"; + this.Name = "SaladOrHouse"; this.AddType(typeof(Salad)); - this.AddType(typeof(Bread)); + this.AddType(typeof(House)); } } ``` -> If you don't supply a name, graphql will automatically use the class name of the proxy as the name of the union. +> If you don't supply a name, graphql will use the class name of the proxy as the name of the union. ## Union Name Uniqueness @@ -115,14 +142,16 @@ Union names must be unique in a schema. If you do declare a union in multiple ac ```csharp title="An Invalid Union Declaration" public class KitchenController : GraphController { - [QueryRoot("searchFood", "SaladOrBread", typeof(Salad), typeof(Bread))] - public async Task SearchFood(string name) + // highlight-next-line + [QueryRoot("search", "SaladOrHouse", typeof(Salad), typeof(House))] + public async Task SearchData(string name) {/* ... */} - // ERROR: Union members for 'SaladAreBread' are different + // ERROR: Union members for 'SaladOrHouse' are different // ----------------- - [QueryRoot("food", "SaladOrBread", typeof(Salad), typeof(Bread), typeof(DinnerRoll))] - public async Task RetrieveSingleFood(int id) + // highlight-next-line + [QueryRoot("fetch", "SaladOrHouse", typeof(Salad), typeof(House), typeof(GameConsole))] + public async Task RetrieveItem(int id) {/* ... */} } ``` @@ -131,6 +160,7 @@ public class KitchenController : GraphController [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. +For Example, the Oven object below can bake any type of bread! ```csharp title="Liskov Substitution Example" public class Bread @@ -144,26 +174,28 @@ public class Bagel : Roll public class Oven { + // highlight-next-line public void Bake(Bread bread) { // We can pass in Bread, Roll or Bagel to the oven. } } ``` -
-However, this presents a problem when when dealing with UNIONs and GraphQL. +However, this presents a problem when when dealing with UNIONs and GraphQL: ```csharp title="BakeryController.cs" public class BakeryController : GraphController { + // highlight-next-line [QueryRoot("searchFood", "RollOrBread", typeof(Roll), typeof(Bread))] public IGraphActionResult SearchFood(string name) { - // Bagle is not a declared member of the union - // but can be used as a Roll and Bread - // which one do we choose? - return this.Ok(new Bagel()); + // Should GraphQL treat a bagel + // as a Roll or Bread ?? + // highlight-next-line + var myBagel = new Bagel(); + return this.Ok(myBagel); } } ``` @@ -182,22 +214,22 @@ 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` or `Bread` yet we return a `Bagel` from the action method. +Most of the time, graphql can correctly interpret the correct 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` or `Bread` yet we return a `Bagel` from the action method. -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. +Since `Bagel` is both a `Roll` and `Bread` which type should graphql match against when executing the inline fragments? 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. #### IGraphUnionProxy.MapType -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 implemented by `IGraphUnionProxy` will be called whenever a query result is indeterminate, allowing you to choose which of your union's allowed types should be used. +Luckily there is a way to allow you to take control of your unions and make the determination on your own. The `MapType` method provided by `IGraphUnionProxy` will be called whenever a query result is indeterminate, allowing you to choose which of your union's allowed types should be used. -```csharp -// RollOrBread.cs +```csharp title="Using a Custom Type Mapper" public class RollOrBread : GraphUnionProxy { public RollOrBread() : base(typeof(Roll), typeof(Bread)) {} + // highlight-start public override Type MapType(Type runtimeObjectType) { if (runtimeObjectType == typeof(Bagel)) @@ -205,29 +237,18 @@ public class RollOrBread : GraphUnionProxy else return typeof(Bread); } -} - -// BakeryController.cs -public class BakeryController : GraphController -{ - [QueryRoot("searchFood", typeof(RollOrBread))] - public IGraphActionResult SearchFood(string name) - { - return this.Ok(new Bagel()); - } + // highlight-end } ``` -:::caution - `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](https://spec.graphql.org/October2021/#sec-Value-Completion)) 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. +If, via your logic you are unable to determine which of your Union's types to use 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. + +Most of the time GraphQL ASP.NET will never call `MapType` on your union proxy. If your union types do not share an inheritance chain, for instance, the method will never be called. -:::info -Most of the time GraphQL ASP.NET will never call `MapType` 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. -::: \ No newline at end of file +:::caution + `MapType` is not based on a resolved value, but only on the `System.Type` that was encountered. 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](https://spec.graphql.org/October2021/#sec-Value-Completion)) will be applied to the query. +::: diff --git a/docusaurus.config.js b/docusaurus.config.js index 85e8c49..4da4fd3 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -7,7 +7,7 @@ const darkCodeTheme = require('prism-react-renderer/themes/palenight'); /** @type {import('@docusaurus/types').Config} */ const config = { title: 'GraphQL ASP.NET', - tagline: 'v0.14.0-beta', + tagline: 'v1.0.0-rc2', url: 'https://graphql-aspnet.github.io', baseUrl: '/', onBrokenLinks: 'throw', diff --git a/src/css/custom.css b/src/css/custom.css index 9a43a50..7c0eb36 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -26,7 +26,7 @@ --ifm-color-primary-light: #29d5b0; --ifm-color-primary-lighter: #32d8b4; --ifm-color-primary-lightest: #4fddbf; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); + --docusaurus-highlighted-code-line-bg: rgba(103, 101, 101, 0.3); } .theme-doc-sidebar-item-category-level-1 .menu__list-item-collapsible a:first-of-type { diff --git a/src/pages/index.js b/src/pages/index.js index 40f443e..ebca5d4 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -16,7 +16,7 @@ function HomepageHeader() {

{siteConfig.title}

{siteConfig.tagline}

-
+
{"// C# Controller \n" + "[GraphRoute(\"groceryStore/bakery\")]\n" + - "public class BakeryController : GraphController \n " + + "public class BakeryController : GraphController \n" + "{ \n" + " [Query(\"pastries/search\")]\n" + " public IEnumerable SearchPastries(string text)\n" + diff --git a/src/pages/index.module.css b/src/pages/index.module.css index 9f71a5d..e352d04 100644 --- a/src/pages/index.module.css +++ b/src/pages/index.module.css @@ -10,14 +10,28 @@ overflow: hidden; } + +.main-buttons { + column-gap: 15px; +} + @media screen and (max-width: 996px) { .heroBanner { padding: 2rem; } + +} + +@media (max-width: 480px) { + + .main-buttons { + justify-content: space-around; + row-gap: 5px; + } } .buttons { display: flex; align-items: center; justify-content: center; -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9e15df3..673e5a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,114 +21,114 @@ resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.2.tgz#daa23280e78d3b42ae9564d12470ae034db51a89" integrity sha512-QCckjiC7xXHIUaIL3ektBtjJ0w7tTA3iqKcAE/Hjn1lZ5omp7i3Y4e09rAr9ZybqirL7AbxCLLq0Ra5DDPKeug== -"@algolia/cache-browser-local-storage@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.2.tgz#d5b1b90130ca87c6321de876e167df9ec6524936" - integrity sha512-FRweBkK/ywO+GKYfAWbrepewQsPTIEirhi1BdykX9mxvBPtGNKccYAxvGdDCumU1jL4r3cayio4psfzKMejBlA== - dependencies: - "@algolia/cache-common" "4.14.2" - -"@algolia/cache-common@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.14.2.tgz#b946b6103c922f0c06006fb6929163ed2c67d598" - integrity sha512-SbvAlG9VqNanCErr44q6lEKD2qoK4XtFNx9Qn8FK26ePCI8I9yU7pYB+eM/cZdS9SzQCRJBbHUumVr4bsQ4uxg== - -"@algolia/cache-in-memory@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.14.2.tgz#88e4a21474f9ac05331c2fa3ceb929684a395a24" - integrity sha512-HrOukWoop9XB/VFojPv1R5SVXowgI56T9pmezd/djh2JnVN/vXswhXV51RKy4nCpqxyHt/aGFSq2qkDvj6KiuQ== - dependencies: - "@algolia/cache-common" "4.14.2" - -"@algolia/client-account@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.14.2.tgz#b76ac1ba9ea71e8c3f77a1805b48350dc0728a16" - integrity sha512-WHtriQqGyibbb/Rx71YY43T0cXqyelEU0lB2QMBRXvD2X0iyeGl4qMxocgEIcbHyK7uqE7hKgjT8aBrHqhgc1w== - dependencies: - "@algolia/client-common" "4.14.2" - "@algolia/client-search" "4.14.2" - "@algolia/transporter" "4.14.2" - -"@algolia/client-analytics@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.14.2.tgz#ca04dcaf9a78ee5c92c5cb5e9c74cf031eb2f1fb" - integrity sha512-yBvBv2mw+HX5a+aeR0dkvUbFZsiC4FKSnfqk9rrfX+QrlNOKEhCG0tJzjiOggRW4EcNqRmaTULIYvIzQVL2KYQ== - dependencies: - "@algolia/client-common" "4.14.2" - "@algolia/client-search" "4.14.2" - "@algolia/requester-common" "4.14.2" - "@algolia/transporter" "4.14.2" - -"@algolia/client-common@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.14.2.tgz#e1324e167ffa8af60f3e8bcd122110fd0bfd1300" - integrity sha512-43o4fslNLcktgtDMVaT5XwlzsDPzlqvqesRi4MjQz2x4/Sxm7zYg5LRYFol1BIhG6EwxKvSUq8HcC/KxJu3J0Q== - dependencies: - "@algolia/requester-common" "4.14.2" - "@algolia/transporter" "4.14.2" - -"@algolia/client-personalization@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.14.2.tgz#656bbb6157a3dd1a4be7de65e457fda136c404ec" - integrity sha512-ACCoLi0cL8CBZ1W/2juehSltrw2iqsQBnfiu/Rbl9W2yE6o2ZUb97+sqN/jBqYNQBS+o0ekTMKNkQjHHAcEXNw== - dependencies: - "@algolia/client-common" "4.14.2" - "@algolia/requester-common" "4.14.2" - "@algolia/transporter" "4.14.2" - -"@algolia/client-search@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.14.2.tgz#357bdb7e640163f0e33bad231dfcc21f67dc2e92" - integrity sha512-L5zScdOmcZ6NGiVbLKTvP02UbxZ0njd5Vq9nJAmPFtjffUSOGEp11BmD2oMJ5QvARgx2XbX4KzTTNS5ECYIMWw== - dependencies: - "@algolia/client-common" "4.14.2" - "@algolia/requester-common" "4.14.2" - "@algolia/transporter" "4.14.2" +"@algolia/cache-browser-local-storage@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.3.tgz#b9e0da012b2f124f785134a4d468ee0841b2399d" + integrity sha512-hWH1yCxgG3+R/xZIscmUrWAIBnmBFHH5j30fY/+aPkEZWt90wYILfAHIOZ1/Wxhho5SkPfwFmT7ooX2d9JeQBw== + dependencies: + "@algolia/cache-common" "4.14.3" + +"@algolia/cache-common@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.14.3.tgz#a78e9faee3dfec018eab7b0996e918e06b476ac7" + integrity sha512-oZJofOoD9FQOwiGTzyRnmzvh3ZP8WVTNPBLH5xU5JNF7drDbRT0ocVT0h/xB2rPHYzOeXRrLaQQBwRT/CKom0Q== + +"@algolia/cache-in-memory@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.14.3.tgz#96cefb942aeb80e51e6a7e29f25f4f7f3439b736" + integrity sha512-ES0hHQnzWjeioLQf5Nq+x1AWdZJ50znNPSH3puB/Y4Xsg4Av1bvLmTJe7SY2uqONaeMTvL0OaVcoVtQgJVw0vg== + dependencies: + "@algolia/cache-common" "4.14.3" + +"@algolia/client-account@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.14.3.tgz#6d7d032a65c600339ce066505c77013d9a9e4966" + integrity sha512-PBcPb0+f5Xbh5UfLZNx2Ow589OdP8WYjB4CnvupfYBrl9JyC1sdH4jcq/ri8osO/mCZYjZrQsKAPIqW/gQmizQ== + dependencies: + "@algolia/client-common" "4.14.3" + "@algolia/client-search" "4.14.3" + "@algolia/transporter" "4.14.3" + +"@algolia/client-analytics@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.14.3.tgz#ca409d00a8fff98fdcc215dc96731039900055dc" + integrity sha512-eAwQq0Hb/aauv9NhCH5Dp3Nm29oFx28sayFN2fdOWemwSeJHIl7TmcsxVlRsO50fsD8CtPcDhtGeD3AIFLNvqw== + dependencies: + "@algolia/client-common" "4.14.3" + "@algolia/client-search" "4.14.3" + "@algolia/requester-common" "4.14.3" + "@algolia/transporter" "4.14.3" + +"@algolia/client-common@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.14.3.tgz#c44e48652b2121a20d7a40cfd68d095ebb4191a8" + integrity sha512-jkPPDZdi63IK64Yg4WccdCsAP4pHxSkr4usplkUZM5C1l1oEpZXsy2c579LQ0rvwCs5JFmwfNG4ahOszidfWPw== + dependencies: + "@algolia/requester-common" "4.14.3" + "@algolia/transporter" "4.14.3" + +"@algolia/client-personalization@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.14.3.tgz#8f71325035aa2a5fa7d1d567575235cf1d6c654f" + integrity sha512-UCX1MtkVNgaOL9f0e22x6tC9e2H3unZQlSUdnVaSKpZ+hdSChXGaRjp2UIT7pxmPqNCyv51F597KEX5WT60jNg== + dependencies: + "@algolia/client-common" "4.14.3" + "@algolia/requester-common" "4.14.3" + "@algolia/transporter" "4.14.3" + +"@algolia/client-search@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.14.3.tgz#cf1e77549f5c3e73408ffe6441ede985fde69da0" + integrity sha512-I2U7xBx5OPFdPLA8AXKUPPxGY3HDxZ4r7+mlZ8ZpLbI8/ri6fnu6B4z3wcL7sgHhDYMwnAE8Xr0AB0h3Hnkp4A== + dependencies: + "@algolia/client-common" "4.14.3" + "@algolia/requester-common" "4.14.3" + "@algolia/transporter" "4.14.3" "@algolia/events@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== -"@algolia/logger-common@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.14.2.tgz#b74b3a92431f92665519d95942c246793ec390ee" - integrity sha512-/JGlYvdV++IcMHBnVFsqEisTiOeEr6cUJtpjz8zc0A9c31JrtLm318Njc72p14Pnkw3A/5lHHh+QxpJ6WFTmsA== +"@algolia/logger-common@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.14.3.tgz#87d4725e7f56ea5a39b605771b7149fff62032a7" + integrity sha512-kUEAZaBt/J3RjYi8MEBT2QEexJR2kAE2mtLmezsmqMQZTV502TkHCxYzTwY2dE7OKcUTxi4OFlMuS4GId9CWPw== -"@algolia/logger-console@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.14.2.tgz#ec49cb47408f5811d4792598683923a800abce7b" - integrity sha512-8S2PlpdshbkwlLCSAB5f8c91xyc84VM9Ar9EdfE9UmX+NrKNYnWR1maXXVDQQoto07G1Ol/tYFnFVhUZq0xV/g== +"@algolia/logger-console@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.14.3.tgz#1f19f8f0a5ef11f01d1f9545290eb6a89b71fb8a" + integrity sha512-ZWqAlUITktiMN2EiFpQIFCJS10N96A++yrexqC2Z+3hgF/JcKrOxOdT4nSCQoEPvU4Ki9QKbpzbebRDemZt/hw== dependencies: - "@algolia/logger-common" "4.14.2" + "@algolia/logger-common" "4.14.3" -"@algolia/requester-browser-xhr@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.2.tgz#a2cd4d9d8d90d53109cc7f3682dc6ebf20f798f2" - integrity sha512-CEh//xYz/WfxHFh7pcMjQNWgpl4wFB85lUMRyVwaDPibNzQRVcV33YS+63fShFWc2+42YEipFGH2iPzlpszmDw== +"@algolia/requester-browser-xhr@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.3.tgz#bcf55cba20f58fd9bc95ee55793b5219f3ce8888" + integrity sha512-AZeg2T08WLUPvDncl2XLX2O67W5wIO8MNaT7z5ii5LgBTuk/rU4CikTjCe2xsUleIZeFl++QrPAi4Bdxws6r/Q== dependencies: - "@algolia/requester-common" "4.14.2" + "@algolia/requester-common" "4.14.3" -"@algolia/requester-common@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.14.2.tgz#bc4e9e5ee16c953c0ecacbfb334a33c30c28b1a1" - integrity sha512-73YQsBOKa5fvVV3My7iZHu1sUqmjjfs9TteFWwPwDmnad7T0VTCopttcsM3OjLxZFtBnX61Xxl2T2gmG2O4ehg== +"@algolia/requester-common@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.14.3.tgz#2d02fbe01afb7ae5651ae8dfe62d6c089f103714" + integrity sha512-RrRzqNyKFDP7IkTuV3XvYGF9cDPn9h6qEDl595lXva3YUk9YSS8+MGZnnkOMHvjkrSCKfoLeLbm/T4tmoIeclw== -"@algolia/requester-node-http@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.14.2.tgz#7c1223a1785decaab1def64c83dade6bea45e115" - integrity sha512-oDbb02kd1o5GTEld4pETlPZLY0e+gOSWjWMJHWTgDXbv9rm/o2cF7japO6Vj1ENnrqWvLBmW1OzV9g6FUFhFXg== +"@algolia/requester-node-http@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.14.3.tgz#72389e1c2e5d964702451e75e368eefe85a09d8f" + integrity sha512-O5wnPxtDRPuW2U0EaOz9rMMWdlhwP0J0eSL1Z7TtXF8xnUeeUyNJrdhV5uy2CAp6RbhM1VuC3sOJcIR6Av+vbA== dependencies: - "@algolia/requester-common" "4.14.2" + "@algolia/requester-common" "4.14.3" -"@algolia/transporter@4.14.2": - version "4.14.2" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.14.2.tgz#77c069047fb1a4359ee6a51f51829508e44a1e3d" - integrity sha512-t89dfQb2T9MFQHidjHcfhh6iGMNwvuKUvojAj+JsrHAGbuSy7yE4BylhLX6R0Q1xYRoC4Vvv+O5qIw/LdnQfsQ== +"@algolia/transporter@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.14.3.tgz#5593036bd9cf2adfd077fdc3e81d2e6118660a7a" + integrity sha512-2qlKlKsnGJ008exFRb5RTeTOqhLZj0bkMCMVskxoqWejs2Q2QtWmsiH98hDfpw0fmnyhzHEt0Z7lqxBYp8bW2w== dependencies: - "@algolia/cache-common" "4.14.2" - "@algolia/logger-common" "4.14.2" - "@algolia/requester-common" "4.14.2" + "@algolia/cache-common" "4.14.3" + "@algolia/logger-common" "4.14.3" + "@algolia/requester-common" "4.14.3" "@ampproject/remapping@^2.1.0": version "2.2.0" @@ -1745,9 +1745,9 @@ "@hapi/hoek" "^9.0.0" "@sideway/formula@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" - integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== "@sideway/pinpoint@^2.0.0": version "2.0.0" @@ -1947,7 +1947,7 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.31": version "4.17.31" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f" integrity sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q== @@ -1957,12 +1957,12 @@ "@types/range-parser" "*" "@types/express@*", "@types/express@^4.17.13": - version "4.17.14" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.14.tgz#143ea0557249bc1b3b54f15db4c81c3d4eb3569c" - integrity sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg== + version "4.17.15" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.15.tgz#9290e983ec8b054b65a5abccb610411953d417ff" + integrity sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.18" + "@types/express-serve-static-core" "^4.17.31" "@types/qs" "*" "@types/serve-static" "*" @@ -2027,9 +2027,9 @@ integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== "@types/node@*": - version "18.11.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.11.tgz#1d455ac0211549a8409d3cdb371cd55cc971e8dc" - integrity sha512-KJ021B1nlQUBLopzZmPBVuGU9un7WJd/W4ya7Ih02B4Uwky5Nja0yGYav2EfYIk0RR2Q9oVhf60S2XR1BCWJ2g== + version "18.11.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.17.tgz#5c009e1d9c38f4a2a9d45c0b0c493fe6cdb4bcb5" + integrity sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng== "@types/node@^17.0.5": version "17.0.45" @@ -2153,9 +2153,9 @@ integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^17.0.8": - version "17.0.16" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.16.tgz#9ad7938c9dbe79a261e17adc1ed491de9999612c" - integrity sha512-Mh3OP0oh8X7O7F9m5AplC+XHYLBWuPKNkGVD3gIZFLFebBnuFI2Nz5Sf8WLvwGxECJ8YjifQvFdh79ubODkdug== + version "17.0.17" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.17.tgz#5672e5621f8e0fca13f433a8017aae4b7a2a03e7" + integrity sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g== dependencies: "@types/yargs-parser" "*" @@ -2314,9 +2314,9 @@ acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1: integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== address@^1.0.1, address@^1.1.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/address/-/address-1.2.1.tgz#25bb61095b7522d65b357baa11bc05492d4c8acd" - integrity sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA== + version "1.2.2" + resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" + integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== aggregate-error@^3.0.0: version "3.1.0" @@ -2373,24 +2373,24 @@ algoliasearch-helper@^3.10.0: "@algolia/events" "^4.0.1" algoliasearch@^4.0.0, algoliasearch@^4.13.1: - version "4.14.2" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.14.2.tgz#63f142583bfc3a9bd3cd4a1b098bf6fe58e56f6c" - integrity sha512-ngbEQonGEmf8dyEh5f+uOIihv4176dgbuOZspiuhmTTBRBuzWu3KCGHre6uHj5YyuC7pNvQGzB6ZNJyZi0z+Sg== - dependencies: - "@algolia/cache-browser-local-storage" "4.14.2" - "@algolia/cache-common" "4.14.2" - "@algolia/cache-in-memory" "4.14.2" - "@algolia/client-account" "4.14.2" - "@algolia/client-analytics" "4.14.2" - "@algolia/client-common" "4.14.2" - "@algolia/client-personalization" "4.14.2" - "@algolia/client-search" "4.14.2" - "@algolia/logger-common" "4.14.2" - "@algolia/logger-console" "4.14.2" - "@algolia/requester-browser-xhr" "4.14.2" - "@algolia/requester-common" "4.14.2" - "@algolia/requester-node-http" "4.14.2" - "@algolia/transporter" "4.14.2" + version "4.14.3" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.14.3.tgz#f02a77a4db17de2f676018938847494b692035e7" + integrity sha512-GZTEuxzfWbP/vr7ZJfGzIl8fOsoxN916Z6FY2Egc9q2TmZ6hvq5KfAxY89pPW01oW/2HDEKA8d30f9iAH9eXYg== + dependencies: + "@algolia/cache-browser-local-storage" "4.14.3" + "@algolia/cache-common" "4.14.3" + "@algolia/cache-in-memory" "4.14.3" + "@algolia/client-account" "4.14.3" + "@algolia/client-analytics" "4.14.3" + "@algolia/client-common" "4.14.3" + "@algolia/client-personalization" "4.14.3" + "@algolia/client-search" "4.14.3" + "@algolia/logger-common" "4.14.3" + "@algolia/logger-console" "4.14.3" + "@algolia/requester-browser-xhr" "4.14.3" + "@algolia/requester-common" "4.14.3" + "@algolia/requester-node-http" "4.14.3" + "@algolia/transporter" "4.14.3" ansi-align@^3.0.0, ansi-align@^3.0.1: version "3.0.1" @@ -2744,9 +2744,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426: - version "1.0.30001436" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001436.tgz#22d7cbdbbbb60cdc4ca1030ccd6dea9f5de4848b" - integrity sha512-ZmWkKsnC2ifEPoWUvSAIGyOYwT+keAaaWPHiQ9DfMqS1t6tfuyFYoWR78TeZtznkEQ64+vGXH9cZrElwR2Mrxg== + version "1.0.30001439" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz#ab7371faeb4adff4b74dad1718a6fd122e45d9cb" + integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A== ccount@^1.0.0: version "1.1.0" @@ -3133,12 +3133,12 @@ css-declaration-sorter@^6.3.1: integrity sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w== css-loader@^6.7.1: - version "6.7.2" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.2.tgz#26bc22401b5921686a10fbeba75d124228302304" - integrity sha512-oqGbbVcBJkm8QwmnNzrFrWTnudnRZC+1eXikLJl0n4ljcfotgRifpg2a1lKy8jTrc4/d9A/ap1GFq1jDKG7J+Q== + version "6.7.3" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.3.tgz#1e8799f3ccc5874fdd55461af51137fcc5befbcd" + integrity sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ== dependencies: icss-utils "^5.1.0" - postcss "^8.4.18" + postcss "^8.4.19" postcss-modules-extract-imports "^3.0.0" postcss-modules-local-by-default "^4.0.0" postcss-modules-scope "^3.0.0" @@ -4059,9 +4059,9 @@ globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: slash "^3.0.0" globby@^13.1.1: - version "13.1.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515" - integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ== + version "13.1.3" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.3.tgz#f62baf5720bcb2c1330c8d4ef222ee12318563ff" + integrity sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw== dependencies: dir-glob "^3.0.1" fast-glob "^3.2.11" @@ -4382,9 +4382,9 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== ignore@^5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" - integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== image-size@^1.0.1: version "1.0.2" @@ -4772,9 +4772,9 @@ json-schema-traverse@^1.0.0: integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== json5@^2.1.2, json5@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" - integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + version "2.2.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.2.tgz#64471c5bdcc564c18f7c1d4df2e2297f2457c5ab" + integrity sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ== jsonfile@^6.0.1: version "6.1.0" @@ -5160,9 +5160,9 @@ node-forge@^1: integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== node-releases@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" - integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + version "2.0.8" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" + integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -5775,10 +5775,10 @@ postcss-zindex@^5.1.0: resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-5.1.0.tgz#4a5c7e5ff1050bd4c01d95b1847dfdcc58a496ff" integrity sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A== -postcss@^8.3.11, postcss@^8.4.14, postcss@^8.4.17, postcss@^8.4.18: - version "8.4.19" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.19.tgz#61178e2add236b17351897c8bcc0b4c8ecab56fc" - integrity sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA== +postcss@^8.3.11, postcss@^8.4.14, postcss@^8.4.17, postcss@^8.4.19: + version "8.4.20" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.20.tgz#64c52f509644cecad8567e949f4081d98349dc56" + integrity sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g== dependencies: nanoid "^3.3.4" picocolors "^1.0.0" @@ -6360,9 +6360,9 @@ run-parallel@^1.1.9: queue-microtask "^1.2.2" rxjs@^7.5.4: - version "7.6.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.6.0.tgz#361da5362b6ddaa691a2de0b4f2d32028f1eb5a2" - integrity sha512-DDa7d8TFNUalGC9VqXvQ1euWNN7sc63TrUCuM9J998+ViviahMIjKSOU7rfcgFOF+FCD71BhDRv4hrFz+ImDLQ== + version "7.8.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" + integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== dependencies: tslib "^2.1.0"