Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions docs/advanced/custom-scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Product> Search(Money minPrice)
{
return _service.RetrieveProducts(
Expand All @@ -41,6 +42,7 @@ public class Money

```graphql title="Using the Money Scalar"
query {
# highlight-next-line
search(minPrice: "$18.45"){
id
name
Expand Down Expand Up @@ -99,42 +101,44 @@ 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<char>)`: A resolver function capable of converting an array of characters into the internal representation of the scalar.
ILeafValueResolver contains a single method:

- `Resolve(ReadOnlySpan<char>)`: 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`
- `With "Σscaped chαracters"`

#### 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -274,7 +278,7 @@ A few points about designing your scalar:
- Scalar types should be simple and work in isolation.
- The `ReadOnlySpan<char>` 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

Expand All @@ -295,24 +299,22 @@ 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
}
}
```


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.
Expand Down
142 changes: 73 additions & 69 deletions docs/advanced/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IGraphActionResult> ExecuteFragSpread(int arg1, string arg2) { /.../ }
}
[DirectiveLocations(DirectiveLocation.FRAGMENT_SPREAD)]
public Task<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) { /.../ }
}
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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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; }
}
```
Expand Down Expand Up @@ -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).
Expand Down
Loading