You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/advanced/custom-scalars.md
+75-81Lines changed: 75 additions & 81 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,7 +13,7 @@ This can be done for any value that can be represented as a simple set of charac
13
13
14
14
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:
15
15
16
-
```csharp
16
+
```csharp title="Declaring a Money Scalar"
17
17
publicclassInventoryController : GraphController
18
18
{
19
19
[QueryRoot("search")]
@@ -49,38 +49,35 @@ query {
49
49
```
50
50
51
51
52
-
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.
52
+
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.
53
53
54
54
## Implement IScalarGraphType
55
55
56
-
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:
56
+
To create a scalar we need to implement `IScalarGraphType` and register it with GraphQL. The methods and properties of `IScalarGraphType` are as follows:
57
57
58
58
```csharp title="IScalarGraphType.cs"
59
59
publicinterfaceIScalarGraphType
60
60
{
61
61
stringName { get; }
62
62
stringInternalName { get; }
63
63
stringDescription { get; }
64
+
stringSpecifiedByUrl { get; }
64
65
TypeKindKind { get; }
65
66
boolPublish { get; }
66
67
ScalarValueTypeValueType { get; }
67
68
TypeObjectType { get; }
68
69
TypeCollectionOtherKnownTypes { get; }
69
70
ILeafValueResolverSourceResolver { get; }
70
-
IScalarValueSerializerSerializer { get; }
71
71
72
+
objectSerialize(objectitem);
73
+
stringSerializeToQueryLanguage(objectitem);
72
74
boolValidateObject(objectitem);
73
75
}
74
76
75
77
publicinterfaceILeafValueResolver
76
78
{
77
79
objectResolve(ReadOnlySpan<char> data);
78
80
}
79
-
80
-
publicinterfaceIScalarValueSerializer
81
-
{
82
-
objectSerialize(objectitem);
83
-
}
84
81
```
85
82
86
83
### IScalarGraphType Members
@@ -91,17 +88,26 @@ public interface IScalarValueSerializer
91
88
-`Kind`: Scalars must always be declared as `TypeKind.SCALAR`.
92
89
-`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`.
93
90
-`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`.
91
+
-`SpecifiedByUrl`: A url, formatted as a string, pointing to information or the specification that defines this scalar. (optional, can be null)
94
92
-`ObjectType`: The primary, internal type representing the scalar in .NET. In our example above we would set this to `typeof(Money)`.
95
93
-`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`).
96
94
-`SourceResolver`: An object that implements `ILeafValueResolver` which can convert raw input data into the scalar's primary `ObjectType`.
97
-
-`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).
98
-
-`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.
99
-
100
-
> `ValidateObject(object)` should not attempt to enforce nullability rules. In general, all scalars should return `true` if the provided object is `null`.
95
+
-`Serialize(object)`: A method that converts an instance of your scalar to a leaf value that is serializable in a query response
96
+
- This method must return a `number`, `string`, `bool` or `null`.
97
+
- When converting to a number this can be any C# number value type (int, float, decimal etc.).
98
+
-`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.
99
+
- This method is used when generated default values for field arguments and input object fields via introspection queries.
100
+
- This method must return a value exactly as it would appear in a schema type definition For example, strings must be surrounded by quotes.
101
+
102
+
-`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.
103
+
104
+
:::note
105
+
`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`.
106
+
:::
101
107
102
108
### ILeafValueResolver Members
103
109
104
-
-`Resolve(ReadOnlySpan<char>)`: A resolver function capable of converting an array of characters into the internal representation of the type.
110
+
-`Resolve(ReadOnlySpan<char>)`: A resolver function capable of converting an array of characters into the internal representation of the scalar.
105
111
106
112
#### Dealing with Escaped Strings
107
113
@@ -113,7 +119,7 @@ Example string data:
113
119
-`"""triple quoted string"""`
114
120
-`"With \"\u03A3scaped ch\u03B1racters\""`;
115
121
116
-
The `StringScalarType` provides a handy static method for unescaping the data if you don't need to do anything special with it, `StringScalarType.UnescapeAndTrimDelimiters`.
122
+
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`.
117
123
118
124
Calling `UnescapeAndTrimDelimiters` with the previous examples produces:
119
125
@@ -123,35 +129,13 @@ Calling `UnescapeAndTrimDelimiters` with the previous examples produces:
123
129
124
130
#### Indicating an Error
125
131
126
-
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.
127
-
128
-
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).
129
-
130
-
### IScalarValueSerializer Members
132
+
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.
131
133
132
-
-`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`.
133
-
- When converting to a number this can be any number value type (int, float, decimal etc.).
134
+
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).
134
135
135
-
> `Serialize(object)` must return a string, any primative number or a boolean.
136
-
137
-
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.
> The `Serialize()` method will only be given an object of the approved types for the scalar or null.
153
-
154
-
---
136
+
:::tip Pro Tip!
137
+
If you throw `UnresolvedValueException` your error message will be delivered verbatim to the requestor as part of the response message instead of being obfuscated.
138
+
:::
155
139
156
140
### Example: Money Scalar
157
141
The completed Money custom scalar type
@@ -176,9 +160,27 @@ The completed Money custom scalar type
@@ -233,33 +224,43 @@ Since our scalar is represented by a .NET class, if we don't pre-register it Gra
233
224
234
225
## @specifiedBy Directive
235
226
236
-
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.
227
+
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.
237
228
238
229
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.
@@ -272,16 +273,13 @@ A few points about designing your scalar:
272
273
- 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.
273
274
- Scalar types should be simple and work in isolation.
274
275
- 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.
275
-
- Scalar types should not track any state or depend on any stateful objects.
276
-
-`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.
276
+
- Scalar types should not track any state, depend on any stateful objects, or attempt to use any sort of dependency injection.
277
+
-`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.
277
278
278
279
### Aim for Fewer Scalars
279
280
280
281
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:
281
282
282
-
<divclass="sideBySideCode hljs">
283
-
<div>
284
-
285
283
```csharp title="Money as an Input Object Graph Type"
286
284
publicclassInventoryController : GraphController
287
285
{
@@ -302,9 +300,6 @@ public class Money
302
300
}
303
301
```
304
302
305
-
</div>
306
-
<div>
307
-
308
303
```graphql title="Using the Money Input Object"
309
304
query {
310
305
search(minPrice: {
@@ -316,10 +311,9 @@ query {
316
311
}
317
312
```
318
313
319
-
</div>
320
-
</div>
321
-
<br/>
322
314
323
315
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.
324
316
325
-
> Creating a custom scalar should be a last resort, not a first option.
317
+
:::caution Be Careful
318
+
Creating a custom scalar should be a last resort, not a first option.
0 commit comments