From e938753207df2aeaf851fe738c09453bfaa3f6c7 Mon Sep 17 00:00:00 2001 From: Kevin Carroll Date: Fri, 30 Dec 2022 14:09:19 -0700 Subject: [PATCH 1/3] WIP, updated scalars page, incremented version --- docs/execution/metrics.md | 6 +++--- docs/reference/how-it-works.md | 2 +- docs/types/scalars.md | 30 ++++++++++++++++++++++++++---- docusaurus.config.js | 2 +- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/docs/execution/metrics.md b/docs/execution/metrics.md index 12f6e9d..41f2424 100644 --- a/docs/execution/metrics.md +++ b/docs/execution/metrics.md @@ -5,7 +5,7 @@ sidebar_label: Query Profiling sidebar_position: 0 --- -GraphQL ASP.NET tracks query metrics through the `IGraphQueryExecutionMetrics` interface attached to each query execution context as its processed by the runtime and allows for tracing and timing of individual fields as they are started and completed. +GraphQL ASP.NET tracks query metrics through the `IQueryExecutionMetrics` interface attached to each query execution context as its processed by the runtime and allows for tracing and timing of individual fields as they are started and completed. The metrics themselves enable 3 levels of tracing: @@ -127,7 +127,7 @@ Just as with [logging](../logging/structured-logging), profiling your queries to Customizing the way metrics are captured is not a trivial task but can be done: -1. Implement `IGraphQueryExecutionMetricsFactory` and register it to your DI container before calling `.AddGraphQL()`. This will override the internal factory and use your implementation to generate metrics packages for any received requests. -2. Implement `IGraphQueryExecutionMetrics` and have your factory return transient instances of this class when requested. +1. Implement `IQueryExecutionMetricsFactory` and register it to your DI container before calling `.AddGraphQL()`. This will override the internal factory and use your implementation to generate metrics packages for any received requests. +2. Implement `IQueryExecutionMetrics` and have your factory return transient instances of this class when requested. The runtime will now send metrics events to your objects and you can proceed with handling the data. However, the default pipeline structure is still only going to deliver 3 named phases to your metrics package (Parsing, Validation, Execution). If you want to alter the phase sequence or add new ones, you'll need to implement your own core pipeline components, which is beyond the scope of this documentation. \ No newline at end of file diff --git a/docs/reference/how-it-works.md b/docs/reference/how-it-works.md index c117fb4..8abf63b 100644 --- a/docs/reference/how-it-works.md +++ b/docs/reference/how-it-works.md @@ -91,7 +91,7 @@ query { _Sample query used as a reference example in this section_ -The supplied query document (top right in the example) is ran through a compilation cycle to ultimately generate an `IGraphQueryPlan`. It is first lexed into a series of tokens representing the various parts; things like curly braces, colons, strings etc. Then it parses those tokens into a collection of `SyntaxNodes` (creating an Abstract Syntax Tree) representing concepts like `FieldNode`, `InputValueNode`, and `OperationTypeNode` following the [graphql specification rules for source text documents](https://spec.graphql.org/October2021/#sec-Source-Text). +The supplied query document (top right in the example) is ran through a compilation cycle to ultimately generate an `IQueryExecutionPlan`. It is first lexed into a series of tokens representing the various parts; things like curly braces, colons, strings etc. Then it parses those tokens into a collection of `SyntaxNodes` (creating an Abstract Syntax Tree) representing concepts like `FieldNode`, `InputValueNode`, and `OperationTypeNode` following the [graphql specification rules for source text documents](https://spec.graphql.org/October2021/#sec-Source-Text). Once parsed, the runtime will execute its internal rules engine against the generated `ISyntaxTree`, using the targeted `ISchema`, to create a query plan where it marries the nodes AST with concrete structures such as controllers, action methods and POCOs. It is at this stage where the `hero` field in the example is matched to the `HeroController` with its appropriate `IGraphFieldResolver` to invoke the `RetrieveHero` action method. diff --git a/docs/types/scalars.md b/docs/types/scalars.md index 9bbbbca..87048b6 100644 --- a/docs/types/scalars.md +++ b/docs/types/scalars.md @@ -38,20 +38,42 @@ GraphQL ASP.NET has 20 built in scalar types. ## Input Value Resolution -When a value is resolved, it's read from the query document (or variable collection) in one of three ways: +When a scalar value is resolved, it's read from the query document (or variable collection) in one of three ways: - **String** : A string of characters, delimited by `"quotes"` -- **Boolean** The value `true` or `false` with no quotes -- **Number** A sequence of numbers with an optional decimal point, negative sign or the letter `e` +- **Boolean** The value `true` or `false` without quotes +- **Number** A sequence of numbers with an optional decimal point, negative sign or the letter `e` without quotes - example: `-123.456e78` - GraphQL numbers must conform to the [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) standard [Spec § [3.5.2](https://graphql.github.io/graphql-spec/October2021/#sec-Float)] Scalars used as input arguments require that any supplied value match at least one supported input format before they will attempt to convert the value into the related .NET type. If the value read from the document doesn't match an approved format it is rejected before conversion is attempted. -For example, the library will accept dates as numbers or strings (e.g. `1670466552`, `"2022-12-8'T'02:29:10"`). If you try to supply a boolean value, `true`, the query is rejected outright and no parsing attempt is made. This can come in handy for custom scalar types that may have multiple serialization options. +For example, the library will accept dates as numbers or strings. If you try to supply a boolean value, `true`, the query is rejected outright and no parsing attempt is made. This can come in handy for custom scalar types that may have multiple serialization options. See the table above for the list of allowed formats per scalar type. +#### Working With Dates + +Date valued scalars (e.g. `DateTime`, `DateTimeOffset`, `DateOnly`) can be supplied as an [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) compliant string value or as a number representing the amount of time from the [Unix Epoch](https://en.wikipedia.org/wiki/Unix_time). + +Examples: + +| Supplied Value | Parsed Date | +|-----------------|-------------| +|`"2022-12-30T18:30:38.259+00:00"` |Dec. 30, 2022 @ 6:30:38.259 PM (UTC - 0) | +|`"2022-12-30"` |Dec. 30, 2022 @ 00:00:00.000 AM (UTC - 0) | +|`1586965574234` | April 15, 2020 @ 3:46:14.234 AM (UTC - 0) | +|`1586940374` | April 15, 2020 @ 3:46:14.000 AM (UTC - 0) | + +:::tip Epoch Values +Dates supplied as an epoch number can be supplied with or without milliseconds. +::: + +> Note: If a time component is supplied to a `DateOnly` scalar, it will be truncated and only the date portion will be used. + +By Default, the library will serialize all dates as an `RFC 3339` compliant string. + + ## Scalar Names Are Fixed Unlike other graph types, scalar names are fixed across all schemas. The name defined above (including casing), is how they appear in your schema's introspection queries. These names conform to the accepted standard for graphql type names. This is true for any custom scalars you may build as well. diff --git a/docusaurus.config.js b/docusaurus.config.js index 4da4fd3..6a86c38 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: 'v1.0.0-rc2', + tagline: 'v1.0.0-rc3', url: 'https://graphql-aspnet.github.io', baseUrl: '/', onBrokenLinks: 'throw', From 9f8c18322a42a75d776c77ab17e350da95cd416d Mon Sep 17 00:00:00 2001 From: Kevin Carroll Date: Fri, 30 Dec 2022 16:39:33 -0700 Subject: [PATCH 2/3] WIP, typos and clarifications --- docs/advanced/directives.md | 142 +++++++++++++++++---------------- docs/advanced/subscriptions.md | 18 +++-- docs/types/list-non-null.md | 4 +- 3 files changed, 85 insertions(+), 79 deletions(-) diff --git a/docs/advanced/directives.md b/docs/advanced/directives.md index d81fb95..643f3e2 100644 --- a/docs/advanced/directives.md +++ b/docs/advanced/directives.md @@ -79,26 +79,26 @@ The following properties are available to all directive action methods: 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 - { - [DirectiveLocations(DirectiveLocation.FIELD)] - public IGraphActionResult ExecuteField(int arg1, string arg2) { /.../ } +public class MyValidDirective : GraphDirective +{ + [DirectiveLocations(DirectiveLocation.FIELD)] + public IGraphActionResult ExecuteField(int arg1, string arg2) { /.../ } - [DirectiveLocations(DirectiveLocation.FRAGMENT_SPREAD)] - public Task ExecuteFragSpread(int arg1, string arg2) { /.../ } - } + [DirectiveLocations(DirectiveLocation.FRAGMENT_SPREAD)] + public Task ExecuteFragSpread(int arg1, string arg2) { /.../ } +} - 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) { /.../ } - } +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) { /.../ } +} ``` :::info @@ -109,25 +109,25 @@ Directives may contain input arguments just like fields. However, its important (_**a.k.a. Operation Directives**_) -Execution Directives are applied to query documents and executed only on single request in which they are encountered. +Execution Directives are applied to query documents and executed only on the request in which they are encountered. ### Example: @include This is the code for the built in `@include` directive: ```csharp - [GraphType("include")] - public sealed class IncludeDirective : GraphDirective +[GraphType("include")] +public sealed class IncludeDirective : GraphDirective +{ + [DirectiveLocations(DirectiveLocation.FIELD | DirectiveLocation.FRAGMENT_SPREAD | DirectiveLocation.INLINE_FRAGMENT)] + public IGraphActionResult Execute([FromGraphQL("if")] bool ifArgument) { - [DirectiveLocations(DirectiveLocation.FIELD | DirectiveLocation.FRAGMENT_SPREAD | DirectiveLocation.INLINE_FRAGMENT)] - public IGraphActionResult Execute([FromGraphQL("if")] bool ifArgument) - { - if (this.DirectiveTarget is IIncludeableDocumentPart idp) - idp.IsIncluded = ifArgument; + if (this.DirectiveTarget is IIncludeableDocumentPart idp) + idp.IsIncluded = ifArgument; - return this.Ok(); - } + return this.Ok(); } +} ``` This Directive: @@ -233,35 +233,35 @@ Type System directives are applied to schema items and executed at start up whil This directive will extend the resolver of a field, as its declared **in the schema**, to turn any strings into lower case letters. ```csharp title="Example: ToLowerDirective.cs" - public class ToLowerDirective : GraphDirective +public class ToLowerDirective : GraphDirective +{ + [DirectiveLocations(DirectiveLocation.FIELD_DEFINITION)] + public IGraphActionResult Execute() { - [DirectiveLocations(DirectiveLocation.FIELD_DEFINITION)] - public IGraphActionResult Execute() + // ensure we are working with a graph field definition and that it returns a string + if (this.DirectiveTarget is IGraphField field) { - // ensure we are working with a graph field definition and that it returns a string - if (this.DirectiveTarget is IGraphField field) - { - // ObjectType represents the .NET Type of the data returned by the field - if (field.ObjectType != typeof(string)) - throw new Exception("This directive can only be applied to string fields"); - - // update the resolver to execute the orignal - // resolver then apply lower casing to the string result - var resolver = field.Resolver.Extend(ConvertToLower); - field.UpdateResolver(resolver); - } - - return this.Ok(); + // ObjectType represents the .NET Type of the data returned by the field + if (field.ObjectType != typeof(string)) + throw new Exception("This directive can only be applied to string fields"); + + // update the resolver to execute the orignal + // resolver then apply lower casing to the string result + var resolver = field.Resolver.Extend(ConvertToLower); + field.UpdateResolver(resolver); } - private static Task ConvertToLower(FieldResolutionContext context, CancellationToken token) - { - if (context.Result is string) - context.Result = context.Result?.ToString().ToLower(); + return this.Ok(); + } - return Task.CompletedTask; - } + private static Task ConvertToLower(FieldResolutionContext context, CancellationToken token) + { + if (context.Result is string) + context.Result = context.Result?.ToString().ToLower(); + + return Task.CompletedTask; } +} ``` This Directive: @@ -280,25 +280,25 @@ This Directive: The `@deprecated` directive is a built in type system directive provided by graphql to indicate deprecation on a field definition or enum value. Below is the code for its implementation. ```csharp - public sealed class DeprecatedDirective : GraphDirective +public sealed class DeprecatedDirective : GraphDirective +{ + [DirectiveLocations(DirectiveLocation.FIELD_DEFINITION | DirectiveLocation.ENUM_VALUE)] + public IGraphActionResult Execute([FromGraphQL("reason")] string reason = "No longer supported") { - [DirectiveLocations(DirectiveLocation.FIELD_DEFINITION | DirectiveLocation.ENUM_VALUE)] - public IGraphActionResult Execute([FromGraphQL("reason")] string reason = "No longer supported") + if (this.DirectiveTarget is IGraphField field) { - if (this.DirectiveTarget is IGraphField field) - { - field.IsDeprecated = true; - field.DeprecationReason = reason; - } - else if (this.DirectiveTarget is IEnumValue enumValue) - { - enumValue.IsDeprecated = true; - enumValue.DeprecationReason = reason; - } - - return this.Ok(); + field.IsDeprecated = true; + field.DeprecationReason = reason; } + else if (this.DirectiveTarget is IEnumValue enumValue) + { + enumValue.IsDeprecated = true; + enumValue.DeprecationReason = reason; + } + + return this.Ok(); } +} ``` This Directive: @@ -350,9 +350,7 @@ Arguments added to the apply directive attribute will be passed to the directive ```csharp title="Applying Directive Arguments" public class Person { - [ApplyDirective( - "deprecated", - "Names don't matter")] + [ApplyDirective("deprecated", "Names don't matter")] public string Name{ get; set; } } ``` @@ -450,6 +448,12 @@ services.AddGraphQL(o => { }); ``` +```graphql title="Person Type Definition" +type Person @scanItem("medium") @scanItem("high") { + name: String +} +``` + ### Understanding the Type System GraphQL ASP.NET builds your schema and all of its types from your controllers and objects. In general, this is done behind the scenes and you do not need to interact with it. However, when applying type system directives you are affecting the generated schema and need to understand the various parts of it. If you have a question don't be afraid to ask on [github](https://github.com/graphql-aspnet/graphql-aspnet). diff --git a/docs/advanced/subscriptions.md b/docs/advanced/subscriptions.md index 78e4115..dcfc7c9 100644 --- a/docs/advanced/subscriptions.md +++ b/docs/advanced/subscriptions.md @@ -142,7 +142,7 @@ public class SubscriptionController : GraphController } ``` -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. +Here the subscription expects that an event is published using a `WidgetInternal` object 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. @@ -177,7 +177,7 @@ If there are scenarios where an event payload should not be shared with a user, ## 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. +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 additional capacity. 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! @@ -357,7 +357,7 @@ 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 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. +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 `WebSocketClientConnection` 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. @@ -369,7 +369,7 @@ When the router receives an event it looks to see which clients are subscribed t 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. -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. +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 control 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 @@ -387,9 +387,9 @@ The max receiver count can easily be set in the 1000s. There is no magic bullet ### Event Multiplication -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. +Think carefully about your production scenarios when you introduce subscriptions into your application. As mentioned above, for each 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` 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. +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 must still 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. @@ -404,7 +404,7 @@ Internally, whenever a subscription server instance receives an event, the route 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 +This log 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 queue remains above 10,000 events. #### Custom Event Alert Thresholds @@ -443,4 +443,6 @@ services.AddGraphQL() .AddSubscriptions(); ``` -> Consider using the built in `SubscriptionClientDispatchQueueAlertSettings` object for a standard implementation of the required interface. +:::tip + Consider using the built in `SubscriptionClientDispatchQueueAlertSettings` object for a standard implementation of the required interface. +::: \ No newline at end of file diff --git a/docs/types/list-non-null.md b/docs/types/list-non-null.md index 329bbc3..1d9a3d8 100644 --- a/docs/types/list-non-null.md +++ b/docs/types/list-non-null.md @@ -31,7 +31,7 @@ GraphQL ASP.NET makes the following assumptions about your data when creating ty - Reference types **can be** null - Value types **cannot be** null -- Nullable value types (e.g. `int?`) **can be** be null +- Nullable value types (e.g. `int?`) **can be** null - When a reference type implements `IEnumerable` it will be expressed as a "list of `TType`" Type Expressions are commonly shown in the GraphQL schema syntax for field definitions. Here are a few examples of a .NET type and its equivalent type expression in schema syntax. @@ -51,7 +51,7 @@ 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 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 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 want to enforce nullability at the query level and 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 type expression of any field or argument by defining a [custom type expression](../advanced/type-expressions) when needed. From e64b72f759aec8dd9507240182752d881539b128 Mon Sep 17 00:00:00 2001 From: Kevin Carroll Date: Sat, 31 Dec 2022 22:04:23 -0700 Subject: [PATCH 3/3] WIP, additional clarifications and typos --- docs/advanced/custom-scalars.md | 38 ++++++++++++++------------ docs/controllers/batch-operations.md | 20 ++++++++------ docs/controllers/field-paths.md | 25 +++++++---------- docs/logging/structured-logging.md | 36 ++++++++++++------------ docs/reference/schema-configuration.md | 4 ++- 5 files changed, 62 insertions(+), 61 deletions(-) diff --git a/docs/advanced/custom-scalars.md b/docs/advanced/custom-scalars.md index bcdc550..4d912a9 100644 --- a/docs/advanced/custom-scalars.md +++ b/docs/advanced/custom-scalars.md @@ -17,6 +17,7 @@ Lets say we wanted to build a scalar called `Money` that can handle both an amou public class InventoryController : GraphController { [QueryRoot("search")] + // highlight-next-line public IEnumerable Search(Money minPrice) { return _service.RetrieveProducts( @@ -41,6 +42,7 @@ public class Money ```graphql title="Using the Money Scalar" query { + # highlight-next-line search(minPrice: "$18.45"){ id name @@ -99,29 +101,31 @@ public interface ILeafValueResolver - 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. +- `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 result. :::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`. + `ValidateObject(object)` should not attempt to enforce nullability rules. In general, all scalars "could be null" depending on their usage in a schema. All scalars should return `true` for a validation result if the provided object is `null`. ::: -### ILeafValueResolver Members +### ILeafValueResolver -- `Resolve(ReadOnlySpan)`: A resolver function capable of converting an array of characters into the internal representation of the scalar. +ILeafValueResolver contains a single method: + +- `Resolve(ReadOnlySpan)`: A resolver function used for converting an array of characters into the internal representation of the scalar. #### Dealing with Escaped Strings The span provided to `ILeafValueResolver.Resolve` will be the raw data read from the query document. If the data represents a string, it will be provided in its delimited format. This means being surrounded by quotes as well as containing escaped characters (including escaped unicode characters): -Example string data: +Example data: - `"quoted string"` - `"""triple quoted string"""` - `"With \"\u03A3scaped ch\u03B1racters\""`; -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`. +The static method `GraphQLStrings.UnescapeAndTrimDelimiters` provides a handy way for unescaping the data if you don't need to do anything special with it. -Calling `UnescapeAndTrimDelimiters` with the previous examples produces: +Calling `GraphQLStrings.UnescapeAndTrimDelimiters` with the previous examples produces: - `quoted string` - `triple quoted string` @@ -129,12 +133,12 @@ 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 it would need to be rejected because `15.R0` cannot be converted to a decimal decimal. +When resolving incoming 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. -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). +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, the library will obfuscate it to a generic message and only expose your exception details if allowed by the [schema configuration](../reference/schema-configuration). :::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. +If you throw the special `UnresolvedValueException` your error message will be delivered verbatim to the requestor as part of the response message instead of being obfuscated. ::: ### Example: Money Scalar @@ -219,7 +223,7 @@ services.AddGraphQL(); ``` :::info -Since our scalar is represented by a .NET class, if we don't pre-register it GraphQL will attempt to parse the `Money` class as an object graph type. Once registered as a scalar, any attempt to use `Money` as an object graph type will cause an exception. +Since our scalar is represented by a .NET class, if we don't pre-register it GraphQL will attempt to parse the `Money` class as an input object graph type. Once registered as a scalar, any attempt to use `Money` as an object graph type will cause an exception. ::: ## @specifiedBy Directive @@ -274,7 +278,7 @@ A few points about designing your scalar: - 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, 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. +- `ILeafValueResolver.Resolve` must be **FAST**! Since your resolver is used to construct an initial query plan from the raw query text, it'll be called many orders of magnitude more often than any other method. ### Aim for Fewer Scalars @@ -295,16 +299,14 @@ public class InventoryController : GraphController public class Money { - public string Symbol { get; } - public decimal Price { get; } + public string Symbol { get; set; } + public decimal Price { get; set; } } ``` ```graphql title="Using the Money Input Object" query { - search(minPrice: { - symbol: "$" - price: 18.45}){ + search(minPrice: {symbol: "$" price: 18.45}){ id name } @@ -312,7 +314,7 @@ 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. +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 applications using our graph may need to be updated. It is almost always better to represent your data as an input object rather than a custom scalar. :::caution Be Careful Creating a custom scalar should be a last resort, not a first option. diff --git a/docs/controllers/batch-operations.md b/docs/controllers/batch-operations.md index 0b96beb..f7a8e86 100644 --- a/docs/controllers/batch-operations.md +++ b/docs/controllers/batch-operations.md @@ -11,7 +11,7 @@ Read the section on [type extensions](./type-extensions) before reading this doc ## The N+1 Problem -There are plenty of articles on the web discussing the theory behind the N+1 problem ([links below](./batch-operations#other-resources)). Instead, we'll jump into an into an example to illustrate the issue when it comes to GraphQL. +There are plenty of articles on the web discussing the theory behind the N+1 problem ([links below](./batch-operations#other-resources)). Instead, we'll jump into an example to illustrate the issue when it comes to GraphQL. Let's build on our example from the discussion on type extensions where we created an extension to retrieve `Cake Orders` for a **single** `Bakery`. What if we're a national chain and need to see the last 50 orders for each of our stores in a region? This seems like a reasonable thing an auditor would do so lets alter our controller to fetch all our bakeries and then let our type extension fetch the cake orders. @@ -25,6 +25,7 @@ public class Bakery public class BakedGoodsCompanyController : GraphController { [QueryRoot("bakeries")] + // highlight-next-line public async Task> RetrieveBakeries(Regions region = Regions.All) {/*...*/} @@ -78,10 +79,12 @@ public class BakedGoodsCompanyController : GraphController var allOrders = await _service.RetrieveCakeOrders(bakeries.Select(x => x.Id), limitTo); // return the batch of orders + // highlight-start return this.StartBatch() .FromSource(bakeries, bakery => bakery.Id) .WithResults(allOrders, cakeOrder => cakeOrder.BakeryId) .Complete(); + // highlight-end } } ``` @@ -94,7 +97,6 @@ Key things to notice: The contents of your extension method is going to vary widely from use case to use case. Here we've forwarded the ids of the bakeries to a service to fetch the orders. The important take away is that `RetrieveCakeOrders` is now called only once, regardless of how many items are in the `IEnumerable` parameter. -GraphQL works behind the scenes to pull together the items generated from the parent field and passes them to your batch method. ## Data Loaders @@ -104,7 +106,7 @@ You'll often hear the term `Data Loaders` when reading about GraphQL implementat `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). -In the example we matched on a bakery's primary key selecting `Bakery.Id` from each of the source items and pairing it against `CakeOrder.BakeryId` from each of the results. This is enough information for the builder to generate a valid result. Depending on the contents of your data, the type expression of your extension there are few scenarios that emerge: +In the example we matched on a bakery's primary key selecting `Bakery.Id` from each of the source items and pairing it against `CakeOrder.BakeryId` from each of the results. This is enough information for the builder to generate a valid result. Depending on the contents of your data and the type expression of your extension there are few scenarios that emerge: **1 Source -> 1 Result** @@ -122,9 +124,11 @@ To GraphQL, many to many relationships are treated the same as one to many. Inte For sibling relationships there are only two options; either the data exists and a value is returned or it doesn't and `null` is returned. But with parent/child relationships, sometimes you want to indicate that no results were _included_ for a parent item and sometimes you want to indicate that no results _exist_. This could be represented as being an empty array vs `null`. When working with children, for every parent supplied to `this.StartBatch`, GraphQL will generate a field result of **at least** an empty array. To indicate a parent item should receive `null` instead of `[]` exclude it from the batch. -> Excluding a source item from `this.StartBatch()` will result in it receiving `null` for its resolved field value. +Note that it is your method's responsibility to be compliant with the type expression of the field. If a field is marked as `NON_NULL` and you exclude the parent item from the batch (resulting in a null result for the field for that item) the field will be marked invalid and register an error. -Note that it is your method's responsibility to be compliant with the type expression of the field in this regard. If a field is marked as `NON_NULL` and you exclude the parent item from the batch (resulting in a null result for the field for that item) the field will be marked invalid and register an error. +:::caution +Excluding a source item from `this.StartBatch()` will result in it receiving `null` for its resolved field value. Be mindful of your extension's type expression. If you've made the field non-nullable an error will be generated. +::: #### Returning `IDictionary` @@ -143,10 +147,10 @@ public class BakedGoodsCompanyController : GraphController public async Task> RetrieveBakeries(Region region){/*...*/} // declare the batch operation as an extension + // highlight-start [BatchTypeExtension(typeof(Bakery), "orders")] - public async Task>> RetrieveCakeOrders( - IEnumerable bakeries, - int limitTo = 15) + public async Task>> RetrieveCakeOrders(IEnumerable bakeries, int limitTo = 15) + // highlight-end { //fetch all the orders at once Dictionary> allOrders = await _service diff --git a/docs/controllers/field-paths.md b/docs/controllers/field-paths.md index 9cbd147..ff5b090 100644 --- a/docs/controllers/field-paths.md +++ b/docs/controllers/field-paths.md @@ -7,7 +7,7 @@ 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. + GraphQL is statically typed. Each field in a query must always resolve to a single graph type known to the schema. In .NET terms that means each field must be represented by a method or property on some class or struct. Traditionally speaking, this can introduce a lot of overhead in defining intermediate types that do nothing but organize our data. Let's think about this query: @@ -34,9 +34,9 @@ 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. +Knowing what we know, you may think we need to create types for the grocery store, the bakery, pastries, a donut, the deli counter, meats, beef etc. in order to create properties and methods for all those fields. 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. -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. +However, with GraphQL ASP.NET, using a templating pattern similar to what we do with REST controllers 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 @@ -55,7 +55,7 @@ public class GroceryStoreController : GraphController } ``` -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. +Internally, for each encountered path segment (e.g. `bakery`, `meats`), GraphQL generates a virutal, intermediate graph type to fulfill resolver requests for you and acts 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 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. @@ -146,7 +146,7 @@ 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 property getter; commonly referred to as its resolver. We can't declare a field twice. +Each field of each type in your schema 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: @@ -154,7 +154,7 @@ Take this example: [GraphRoute("bakery")] public class BakeryController : GraphController { - // Both Methods represent the same 'orderDonuts' field on the object graph + // Both Methods represent the same 'orderDonuts' field on the graph [Mutation] // highlight-next-line @@ -184,10 +184,7 @@ public class BakeryController : GraphController public Manager OrderDonuts(string type, int quantity){/*...*/} } ``` - -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, 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. +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. @@ -238,7 +235,7 @@ public class BakeryController : GraphController ```graphql title="Sample Queries" mutation { - orderDonuts(count: 12){ + orderDonuts(count: 12) { name flavor } @@ -246,9 +243,7 @@ mutation { mutation { bakery { - orderDonuts( - type: "Chocolate" - count: 3){ + orderDonuts(type: "Chocolate" count: 3) { name flavor } @@ -347,4 +342,4 @@ mutation { } ``` -You can alter the naming formats for fields, enum values and graph types using the declaration options on your [schema configuration](../reference/schema-configuration). +You can alter the naming formats for fields, enum values and graph types using the declaration options on your [schema configuration](../reference/schema-configuration#graphnamingformatter). diff --git a/docs/logging/structured-logging.md b/docs/logging/structured-logging.md index 2da5c90..a3e198c 100644 --- a/docs/logging/structured-logging.md +++ b/docs/logging/structured-logging.md @@ -22,13 +22,14 @@ service.AddGraphQL(/*...*/); Its common practice to inject an instance of `ILogger` or `ILoggerFactory` into a controller in order to log various events of your controller methods. -This is fully supported but GraphQL can also generate an instance of `IGraphLogger` with a few helpful methods for raising "on the fly" log entries if you wish to make use of it. `IGraphLogger` inherits from `ILogger`, the two can be used interchangeably as needed. +This is fully supported but the library can also generate an instance of `IGraphLogger` with a few helpful methods for raising "on the fly" log entries if you wish to make use of it. `IGraphLogger` implements `ILogger`, the two can be used interchangeably as needed. ```csharp title="Using IGraphLogger" public class BakeryController : GraphController { private IGraphLogger _graphLogger; private IDonutService _service; + // highlight-next-line public BakeryController(IDonutService service, IGraphLogger graphLogger) { _service = service; @@ -40,17 +41,22 @@ public class BakeryController : GraphController { Donut donut = _service.CreateDonut(name); + // highlight-start var donutEvent = new GraphLogEntry("New Donut Created!"); donutEvent["Name"] = name; donutEvent["Id"] = donut.Id; _graphLogger.Log(LogLevel.Information, donutEvent); + // highlight-end + return donut; } } ``` -> `GraphLogEntry` is an untyped implementation of `IGraphLogEntry` and can be used on the fly for quick operations. +:::tip + `GraphLogEntry` is an untyped implementation of `IGraphLogEntry` and can be used on the fly for quick operations or as a basis for custom log entries. +::: ## Custom ILoggers @@ -61,23 +67,14 @@ By default, the log events will return a contextual message on `.ToString()` wit But given the extra data the log entries contain, it makes more sense to create a custom `ILogger` to take advantage of the full object. ```csharp title="Custom ILogger" -using Microsoft.Extensions.Logging; -using GraphQL.AspNet.Interfaces.Logging; public class MyCustomLogger : ILogger { - public IDisposable BeginScope(TState state) - { - // ... - } - - public bool IsEnabled(LogLevel logLevel) - { - // ... - } + // other code ommited for brevity public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { + // highlight-next-line if (state is IGraphLogEntry logEntry) { // handle the log entry here @@ -87,7 +84,7 @@ public class MyCustomLogger : ILogger ``` :::info - The state parameter of `ILogger.Log` will be an instance of `IGraphLogEntry` whenever a GraphQL ASP.NET log event is recorded. + The state parameter of `ILogger.Log()` will be an instance of `IGraphLogEntry` whenever a GraphQL ASP.NET log event is recorded. ::: ## Log Entries are KeyValuePair Collections @@ -95,10 +92,7 @@ public class MyCustomLogger : ILogger While the various [standard log events](./standard-events) declare explicit properties for the data they return, every log entry is just a collection of key/value pairs that can be iterated through for quick serialization. ```csharp -public interface IGraphLogEntry : IGraphLogPropertyCollection -{ /*...*/ } - -public interface IGraphLogPropertyCollection : IEnumerable> +public interface IGraphLogEntry : IEnumerable> { /*...*/ } ``` @@ -123,7 +117,11 @@ Here we've enabled the log events through `appsettings.json` } ``` -Log Entries are not allocated unless their respective log levels are enabled. It is not uncommon for real world queries to generate 100s of log entries per request. Take care to ensure you have enabled or disabled logging appropriately in your environment as it can greatly impact performance if left unchecked. It is never a good idea to enable trace level logging for graphQL outside of development. +Log Entries are not allocated unless their respective log levels are enabled. It is not uncommon for real world queries to generate 100s of log entries per request. Take care to ensure you have setup your logging appropriately in a given environment as it can greatly impact performance if left on by accident. + +:::caution +It is never a good idea to enable trace level logging outside of development. +::: ### Scoped Log Entries diff --git a/docs/reference/schema-configuration.md b/docs/reference/schema-configuration.md index a84faec..8e24b61 100644 --- a/docs/reference/schema-configuration.md +++ b/docs/reference/schema-configuration.md @@ -166,7 +166,9 @@ An object that will format any string to an acceptable name for use in the graph | Enum Values | All Caps | `CHOCOLATE`, `FEET`, `PHONE_NUMBER` | _Default formats for the three different entity types_ -> To make radical changes to your name formats, beyond the available options, inherit from `GraphNameFormatter` and override the different formatting methods. +:::tip + To make radical changes to your name formats, beyond the available options, inherit from `GraphNameFormatter` and override the different formatting methods. +::: ## Execution Options