Skip to content

Commit 4c20834

Browse files
Doc Updates for 1.0.0-RC2 (#36)
* Fixed multiple highlighting issues across many pages * Fixed interface names for RC1 and RC2 name updates * Typos and clarifications
1 parent c7b65e7 commit 4c20834

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1375
-1090
lines changed

docs/advanced/custom-scalars.md

Lines changed: 75 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ This can be done for any value that can be represented as a simple set of charac
1313

1414
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:
1515

16-
```csharp
16+
```csharp title="Declaring a Money Scalar"
1717
public class InventoryController : GraphController
1818
{
1919
[QueryRoot("search")]
@@ -49,38 +49,35 @@ query {
4949
```
5050

5151

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.
5353

5454
## Implement IScalarGraphType
5555

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:
5757

5858
```csharp title="IScalarGraphType.cs"
5959
public interface IScalarGraphType
6060
{
6161
string Name { get; }
6262
string InternalName { get; }
6363
string Description { get; }
64+
string SpecifiedByUrl { get; }
6465
TypeKind Kind { get; }
6566
bool Publish { get; }
6667
ScalarValueType ValueType { get; }
6768
Type ObjectType { get; }
6869
TypeCollection OtherKnownTypes { get; }
6970
ILeafValueResolver SourceResolver { get; }
70-
IScalarValueSerializer Serializer { get; }
7171

72+
object Serialize(object item);
73+
string SerializeToQueryLanguage(object item);
7274
bool ValidateObject(object item);
7375
}
7476

7577
public interface ILeafValueResolver
7678
{
7779
object Resolve(ReadOnlySpan<char> data);
7880
}
79-
80-
public interface IScalarValueSerializer
81-
{
82-
object Serialize(object item);
83-
}
8481
```
8582

8683
### IScalarGraphType Members
@@ -91,17 +88,26 @@ public interface IScalarValueSerializer
9188
- `Kind`: Scalars must always be declared as `TypeKind.SCALAR`.
9289
- `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`.
9390
- `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)
9492
- `ObjectType`: The primary, internal type representing the scalar in .NET. In our example above we would set this to `typeof(Money)`.
9593
- `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`).
9694
- `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+
:::
101107

102108
### ILeafValueResolver Members
103109

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.
105111

106112
#### Dealing with Escaped Strings
107113

@@ -113,7 +119,7 @@ Example string data:
113119
- `"""triple quoted string"""`
114120
- `"With \"\u03A3scaped ch\u03B1racters\""`;
115121

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`.
117123

118124
Calling `UnescapeAndTrimDelimiters` with the previous examples produces:
119125

@@ -123,35 +129,13 @@ Calling `UnescapeAndTrimDelimiters` with the previous examples produces:
123129

124130
#### Indicating an Error
125131

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.
131133

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).
134135

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.
138-
139-
```csharp title="GuidScalarSerializer.cs"
140-
public class GuidScalarSerializer : IScalarValueSerializer
141-
{
142-
public object Serialize(object item)
143-
{
144-
if (item == null)
145-
return item;
146-
147-
return ((Guid)item).ToString();
148-
}
149-
}
150-
```
151-
152-
> 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+
:::
155139

156140
### Example: Money Scalar
157141
The completed Money custom scalar type
@@ -176,9 +160,27 @@ The completed Money custom scalar type
176160

177161
public TypeCollection OtherKnownTypes => TypeCollection.Empty;
178162

179-
public ILeafValueResolver SourceResolver { get; } = new MoneyLeafTypeResolver();
163+
public ILeafValueResolver SourceResolver { get; } = new MoneyValueResolver();
164+
165+
public object Serialize(object item)
166+
{
167+
if (item == null)
168+
return item;
169+
170+
var money = (Money)item;
171+
return $"{money.Symbol}{money.Price}";
172+
}
173+
174+
public string SerializeToQueryLanguage(object item)
175+
{
176+
// convert to a string first
177+
var serialized = this.Serialize(item);
178+
if (serialized == null)
179+
return "null";
180180

181-
public IScalarValueSerializer Serializer { get; } = new MoneyScalarTypeSerializer()
181+
// return value as quoted
182+
return $"\"{serialized}\"";
183+
}
182184

183185
public bool ValidateObject(object item)
184186
{
@@ -189,30 +191,19 @@ The completed Money custom scalar type
189191
}
190192
}
191193

192-
public class MoneyLeafTypeResolver : ILeafValueResolver
194+
public class MoneyValueResolver : ILeafValueResolver
193195
{
194196
public object Resolve(ReadOnlySpan<char> data)
195197
{
196198
// example only, more validation code is needed to fully validate
197199
// the data
198-
var sanitizedMoney = StringScalarType.UnescapeAndTrimDelimiters(data);
200+
var sanitizedMoney = GraphQLStrings.UnescapeAndTrimDelimiters(data);
199201
if(sanitizedMoney == null || sanitizedMoney.Length < 2)
200202
throw new UnresolvedValueException("Money must be at least 2 characters");
201203

202204
return new Money(sanitizedMoney[0], Decimal.Parse(sanitizedMoney.Substring(1)));
203205
}
204206
}
205-
public class MoneyScalarTypeSerializer : IScalarValueSerializer
206-
{
207-
public override object Serialize(object item)
208-
{
209-
if (item == null)
210-
return item;
211-
212-
var money = (Money)item;
213-
return $"{money.Symbol}{money.Price}";
214-
}
215-
}
216207
```
217208

218209
## Registering A Scalar
@@ -233,33 +224,43 @@ Since our scalar is represented by a .NET class, if we don't pre-register it Gra
233224

234225
## @specifiedBy Directive
235226

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.
237228

238229
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.
239230

240-
```csharp title="Apply the @specifiedBy"
241-
// apply the directive to a single schema
242-
GraphQLProviders.ScalarProvider.RegisterCustomScalar(typeof(MoneyScalarType));
231+
```csharp title="Applying the @specifiedBy"
232+
// apply the directive to a single schema at startup
243233
services.AddGraphQL(o => {
234+
// highlight-start
244235
o.ApplyDirective("@specifiedBy")
245236
.WithArguments("https://myurl.com")
246237
.ToItems(item => item.Name == "Money");
238+
// highlight-end
247239
});
248240

249-
// via the ApplyDirective attribute
241+
// via the [ApplyDirective] attribute
250242
// for all schemas
243+
// highlight-next-line
251244
[ApplyDirective("@specifiedBy", "https://myurl.com")]
252-
public class MoneyScalarType : IScalarType
253-
{
254-
// ...
255-
}
245+
public class MoneyScalarType : IScalarGraphType
246+
{}
256247

257-
// via the special SpecifiedBy attribute
248+
// via the special [SpecifiedBy] attribute
258249
// for all schemas
250+
// highlight-next-line
259251
[SpecifiedBy("https://myurl.com")]
260-
public class MoneyScalarType : IScalarType
252+
public class MoneyScalarType : IScalarGraphType
253+
{}
254+
255+
// as part of the contructor
256+
// for all schemas
257+
public class MoneyScalarType : IScalarGraphType
261258
{
262-
// ...
259+
public MoneyScalarType()
260+
{
261+
// highlight-next-line
262+
this.SpecifiedByUrl = "https://myurl.com";
263+
}
263264
}
264265
```
265266

@@ -272,16 +273,13 @@ A few points about designing your scalar:
272273
- 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.
273274
- Scalar types should be simple and work in isolation.
274275
- 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.
277278

278279
### Aim for Fewer Scalars
279280

280281
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:
281282

282-
<div class="sideBySideCode hljs">
283-
<div>
284-
285283
```csharp title="Money as an Input Object Graph Type"
286284
public class InventoryController : GraphController
287285
{
@@ -302,9 +300,6 @@ public class Money
302300
}
303301
```
304302

305-
</div>
306-
<div>
307-
308303
```graphql title="Using the Money Input Object"
309304
query {
310305
search(minPrice: {
@@ -316,10 +311,9 @@ query {
316311
}
317312
```
318313

319-
</div>
320-
</div>
321-
<br/>
322314

323315
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.
324316

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.
319+
:::

0 commit comments

Comments
 (0)