From c96b6c4cd7c165e0e6581b27204ec30f3dd3adfb Mon Sep 17 00:00:00 2001 From: Kevin Carroll Date: Thu, 18 Aug 2022 17:53:42 -0700 Subject: [PATCH 1/2] updated input object documentation --- docs/types/input-objects.md | 70 ++++++++++++++++++++++++++++++------- website/pages/en/index.js | 2 +- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/docs/types/input-objects.md b/docs/types/input-objects.md index 43f176f..a68ed3a 100644 --- a/docs/types/input-objects.md +++ b/docs/types/input-objects.md @@ -4,17 +4,13 @@ title: Input Objects sidebar_label: Input Objects --- -`INPUT_OBJECT` graph types function in much the same way as [object graph types](./objects) do. -While GraphQL is doing its discovery of controllers and graph types, whenever it comes across a class used as a parameter to a method it will attempt to generate the appropriate input type definition. +`INPUT_OBJECT` graph types represent complex data supplied to field arguments or directives. Anytime you want to pass more data than a single string or a number, perhaps an Address or a new Employee, you use an INPUT_OBJECT to represent that entity in GraphQL. When the system scans your controllers, if it comes across a class used as a parameter to a method it will attempt to generate the appropriate input type definition to represent that class. -The rules surrounding naming, field declarations, exclusions, use of `[GraphSkip]` etc. apply to input objects but with a few key differences. +The rules surrounding naming, field declarations, exclusions, use of `[GraphSkip]` etc. apply to input objects but with a few key differences: -By Default: - -- An input object is named the same as its `class` name, prefixed with `Input_` - - i.e. `Input_Donut`, `Input_Employee` -- All public properties with a `get` and `set` statement will be included. - - The return type of the property must be an acceptable type or it will be skipped +- Unless overridden, an input object is named the same as its `class` name, prefixed with `Input_` (e.g. `Input_Address`, `Input_Employee`) +- Only public properties with a `get` and `set` will be included. + - Properties cannot return a `Task`, an `interface` and cannot implements `IGraphUnionProxy` or `IGraphActionResult`. Such properties are always skipped. - Methods are always skipped. ## Names @@ -78,7 +74,7 @@ public class DonutModel } ``` -Because of this restriction it can be helpful to separate your classes between "input" and "output" types much is the same way we do with `ViewModel` vs. `BindingModel` objects in MVC. This is optional, mix and match as needed by your use case. +Because of this restriction it can be helpful to separate your classes between "input" and "output" types much is the same way we do with `ViewModel` vs. `BindingModel` objects with REST queries in ASP.NET. This is optional, mix and match as needed by your use case. ## Properties Must Have a Public Setter @@ -158,9 +154,59 @@ 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. The type expression will also automatically become non-nullable if it otherwise would have been nullable and no default value will be assigned to the field. For example string fields are nullable by default, adding `[Required]` will convert the type expression of the property to `String!` automatically. + +Any non-required field will automatically be assigned a default value that will be made available to introspection queries. This default value is equivilant to the property value of the object when the object is instantiated via its default constructor. Use the constructor to set any default values you wish to surface. + +
+
+ +```csharp +// Donut.cs +public class Donut +{ + public Donut() + { + this.Type = "Vanilla"; + this.Price = 2.99; + this.IsAvailable = true; + } + + [Required] + public string Name { get; set; } + + public int Id { get; set; } + public string Type { get; set; } + public Bakery Bakery { get;set; } + public decimal Price { get; set; } + public decimal IsAvailable { get; set; } +} +``` + +
+
+ +```ruby +# GraphQL Type Definition +input Input_Donut { + name: String! + id: Int! = 0 + type: DonutType! = "Vanilla" + bakery: Input_Bakery = null + price: Decimal! = 2.99 + isAvailable: Boolean! = true +} +``` + +
+
+
+ + ## Working With Lists -When constructing a set of items as an input value, GraphQL will instantiate a `List` and fill it with the appropriate data, be that another list, another input object or a scalar. While you can declare a regular array (e.g. `Donut[]`, `int[]` etc.) as your list structure for an input argument, graphql has to rebuild its internal list structure as an array (or nested arrays) to meet the requirements of your method. In some cases, especially with nested lists, this results in a linear increase in processing time. It is recommended to use `IEnumerable` or `IList` to avoid this performance bottleneck when sending a lot of items as input arguments. +When constructing a set of items as an input value, GraphQL will instantiate a `List` and fill it with the appropriate data, be that another list, another input object or a scalar. 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 a linear increase in processing time. It is recommended to use `IEnumerable` or `IList` to avoid this performance bottleneck when sending a lot of items as input arguments. This example shows various ways of accepting collections of data as inputs to controller actions. @@ -203,4 +249,4 @@ public class DonutCollection public List Donuts { get; set; } } -``` +``` \ No newline at end of file diff --git a/website/pages/en/index.js b/website/pages/en/index.js index 42a7c09..3418b80 100644 --- a/website/pages/en/index.js +++ b/website/pages/en/index.js @@ -31,7 +31,7 @@ class HomeSplash extends React.Component {

{GraphQL ASP.NET} {/*{siteConfig.tagline}*/} - v0.11.0-beta + v0.12.0-beta

); From eb29d0cde87be17b4e9a672f66164d0c7a2d2fc7 Mon Sep 17 00:00:00 2001 From: Kevin Carroll Date: Sat, 20 Aug 2022 19:18:08 -0700 Subject: [PATCH 2/2] WIP, additional updates to action results, actions, input object docs --- docs/advanced/graph-action-results.md | 12 +- docs/controllers/actions.md | 55 ++++++++- docs/types/input-objects.md | 155 +++++++++++++++++++------- 3 files changed, 172 insertions(+), 50 deletions(-) diff --git a/docs/advanced/graph-action-results.md b/docs/advanced/graph-action-results.md index 1f57471..ae3b0f4 100644 --- a/docs/advanced/graph-action-results.md +++ b/docs/advanced/graph-action-results.md @@ -40,7 +40,7 @@ public class BakeryController : Controller You can either return the data itself or some alternate `IActionResult` to tell MVC how to render a response. -Common Action Results for MVC: +Some common ASP.NET MVC action results: - `this.Ok()` : Everything worked fine, return status 200. - `this.NotFound()` : The item doesn't exist, return status 404. @@ -49,7 +49,7 @@ Common Action Results for MVC: This works the same way in GraphQL ASP.NET. The available actions are slightly different (GraphQL won't stream files) but the usage is the same. You can even write your own action results. -## Common Graph Action Results +## Common Action Results Instead of `IActionResult` we use `IGraphActionResult` from a controller action method. Both [directives](../directives) and controller [action methods](../controllers/actions) can return action results. @@ -62,9 +62,13 @@ 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. -[Directives](../directives) have one Additional Action Result: +## Directive Action Results +[Directives](../directives) have two built in Action Results: -- `this.Cancel()`: When returned as part of a method that executes before field resolution, this action will cancel the field execution pipeline. No error is returned, but the field is dropped from the request. +- `this.Ok()`: Indicates that the directive completed its expected operation successfully and query processing can continue. +- `this.Cancel()`: Indicates that the directive did NOT complete its operation successfully. + - 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. ## Custom Graph Action Results diff --git a/docs/controllers/actions.md b/docs/controllers/actions.md index 214a73e..a8032f2 100644 --- a/docs/controllers/actions.md +++ b/docs/controllers/actions.md @@ -418,7 +418,7 @@ query {
-Note that there is a difference between "nullable" and "not required". 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. +Note that there is a difference between "nullable" and "not required". 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.
@@ -491,6 +491,53 @@ 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 a linear 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. + +This example shows various ways of accepting collections of data as inputs to controller actions. + +```csharp +public class BakeryController : GraphController +{ + // a list of donuts + // schema syntax: [Donut] + [Mutation("createDonuts")] + public bool CreateDonuts(IEnumerable donuts) + {/*....*/} + + // when used as a "list of list" + // schema syntax: [[Donut]] + [Mutation("createDonutsBySet")] + public bool CreateDonuts(List> donuts) + {/*....*/} + + // when supplied as a regular array + // schema syntax: [Donut] + [Mutation("donutsAsAnArray")] + public bool DonutsAsAnArray(Donut[] donuts) + {/*....*/} + + // This is a valid nested list + // schema syntax: [[[Donut]]] + [Mutation("mixedDonuts")] + public bool MixedDonuts(List> donuts) + {/*....*/} + + // when used as a field of another input object + [Mutation("createDonutCollection")] + public bool CreateDonuts(DonutCollection donutCollection) + {/*....*/} + +} + +public class DonutCollection +{ + public List Donuts { get; set; } +} + +``` + ### Don't Use Dictionaries You might be tempted to use a dictionary as a parameter to accept arbitrary key value pairs into your methods. GraphQL will reject it and throw a declaration exception when your schema is created: @@ -501,7 +548,7 @@ You might be tempted to use a dictionary as a parameter to accept arbitrary key ```csharp public class BakeryController : GraphController { - // ERROR, a GraphDeclarationException + // ERROR, a GraphTypeDeclarationException // will be thrown. [QueryRoot] public IEnumerable @@ -515,7 +562,7 @@ public class BakeryController : GraphController ```javascript query { - searchDonuts( + searchDonuts(searchParams: name: "jelly*" filled: true dayOld: false){ @@ -529,7 +576,7 @@ query {
-At runtime, GraphQL will try to validate every parameter passed on a query against the type expression it has stored in the target schema. No where have we we declared `filled` to be a boolean or `name` to be a string. +At runtime, GraphQL will try to validate every parameter passed on a query against the type expression it has stored in the target schema. 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: diff --git a/docs/types/input-objects.md b/docs/types/input-objects.md index a68ed3a..8fda79a 100644 --- a/docs/types/input-objects.md +++ b/docs/types/input-objects.md @@ -4,18 +4,18 @@ title: Input Objects sidebar_label: Input Objects --- -`INPUT_OBJECT` graph types represent complex data supplied to field arguments or directives. Anytime you want to pass more data than a single string or a number, perhaps an Address or a new Employee, you use an INPUT_OBJECT to represent that entity in GraphQL. When the system scans your controllers, if it comes across a class used as a parameter to a method it will attempt to generate the appropriate input type definition to represent that class. +`INPUT_OBJECT` graph types (a.k.a. input objects) represent complex data supplied to arguments on fields or directives. Anytime you want to pass more data than a single string or a number, perhaps an Address or a new Employee, you use an INPUT_OBJECT to represent that entity in GraphQL. When the system scans your controllers, if it comes across a class used as a parameter to a method it will attempt to generate the appropriate input type definition to represent that class. The rules surrounding naming, field declarations, exclusions, use of `[GraphSkip]` etc. apply to input objects but with a few key differences: -- Unless overridden, an input object is named the same as its `class` name, prefixed with `Input_` (e.g. `Input_Address`, `Input_Employee`) +- Unless overridden, an input object is named the same as its class name, prefixed with `Input_` (e.g. `Input_Address`, `Input_Employee`) - Only public properties with a `get` and `set` will be included. - - Properties cannot return a `Task`, an `interface` and cannot implements `IGraphUnionProxy` or `IGraphActionResult`. Such properties are always skipped. + - Property return types cannot be `Task`, an `interface` and cannot implement `IGraphUnionProxy` or `IGraphActionResult`. Such properties are always skipped. - Methods are always skipped. -## Names +## Customized Type Names -Input object types can be given customized names, just like with object types, using the `[GraphType]` attribute. +Input objects can be given customized names, just like with object types, using the `[GraphType]` attribute.
@@ -49,6 +49,8 @@ input NewDonutModel {

+>Not the specific callout to `InputName` in the attribution. + ## Use an Empty Constructor When GraphQL executes a query it will attempt to create an instance of your input object then assign the key/value pairs received on the query to the properties. In order to do the initial instantiation it requires a public parameterless constructor to do so. @@ -124,7 +126,8 @@ 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; } @@ -155,9 +158,9 @@ 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. The type expression will also automatically become non-nullable if it otherwise would have been nullable and no default value will be assigned to the field. For example string fields are nullable by default, adding `[Required]` will convert the type expression of the property to `String!` automatically. +Add `[Required]` (from System.ComponentModel) to any property to force a user to supply the field in a query document. -Any non-required field will automatically be assigned a default value that will be made available to introspection queries. This default value is equivilant to the property value of the object when the object is instantiated via its default constructor. Use the constructor to set any default values you wish to surface. +Any non-required field will automatically be assigned a default value if not supplied. This default value is equivilant to the property value of the object when its instantiated via its public, parameterless constructor.
@@ -168,7 +171,8 @@ public class Donut { public Donut() { - this.Type = "Vanilla"; + // set custom defaults if needed + this.Type = DonutType.Vanilla; this.Price = 2.99; this.IsAvailable = true; } @@ -177,7 +181,7 @@ public class Donut public string Name { get; set; } public int Id { get; set; } - public string Type { get; set; } + public DonutType Type { get; set; } public Bakery Bakery { get;set; } public decimal Price { get; set; } public decimal IsAvailable { get; set; } @@ -192,7 +196,7 @@ public class Donut input Input_Donut { name: String! id: Int! = 0 - type: DonutType! = "Vanilla" + type: DonutType! = VANILLA bakery: Input_Bakery = null price: Decimal! = 2.99 isAvailable: Boolean! = true @@ -203,50 +207,117 @@ 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 + -## Working With Lists +
+ +```csharp +// Donut.cs +public class Bakery +{ + // a reference to another object + public Person Owner { get; set; } +} +``` -When constructing a set of items as an input value, GraphQL will instantiate a `List` and fill it with the appropriate data, be that another list, another input object or a scalar. 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 a linear increase in processing time. It is recommended to use `IEnumerable` or `IList` to avoid this performance bottleneck when sending a lot of items as input arguments. +```ruby +# GraphQL Type Definition +input Input_Donut { + owner: Input_Person = null +} +``` + +
+
-This example shows various ways of accepting collections of data as inputs to controller actions. +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. + + +
```csharp -public class BakeryController : GraphController +// Donut.cs +public class Bakery { - // a list of donuts - // schema syntax: [Donut] - [Mutation("createDonuts")] - public bool CreateDonuts(IEnumerable donuts) - {/*....*/} + public Bakery() + { + this.Owner = new Person("Bob Smith"); + } - // when used as a "list of list" - // schema syntax: [[Donut]] - [Mutation("createDonutsBySet")] - public bool CreateDonuts(List> donuts) - {/*....*/} + // a reference to another object + [GraphField(TypeExpression = TypeExpressions.IsNotNull)] + public Person Owner { get; set; } +} +``` +```ruby +# GraphQL Type Definition +input Input_Donut { + owner: Input_Person! = { name: "Bob Smith" } +} +``` +
+
- // when supplied as a regular array - // schema syntax: [Donut] - [Mutation("donutsAsAnArray")] - public bool DonutsAsAnArray(Donut[] donuts) - {/*....*/} - // This is a valid nested list - // schema syntax: [[[Donut]]] - [Mutation("mixedDonuts")] - public bool MixedDonuts(List> donuts) - {/*....*/} +> 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`. - // when used as a field of another input object - [Mutation("createDonutCollection")] - public bool CreateDonuts(DonutCollection donutCollection) - {/*....*/} +Add the [Required] attribute to force a user to supply a non-null value for the field on a query document. + + +
+ +```csharp +// Donut.cs +public class Bakery +{ + public Bakery() + { + } + + // a reference to another object + [Required] + [[GraphField(TypeExpression = TypeExpressions.IsNotNull)]] + public Person Owner { get; set; } +} +``` +```ruby +# GraphQL Type Definition +input Input_Donut { + owner: Input_Person! +} +``` +
+
+ +## 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 Values + +Take a look at this example of an enum and input object: + +```csharp +public class Donut +{ + public string Name{ get; set; } + public DonutFlavor Flavor { get; set; } } -public class DonutCollection +public enum DonutFlavor { - public List Donuts { get; set; } + [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's underlying data type (int). 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. -``` \ No newline at end of file +> 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.