Skip to content

Commit 22a8658

Browse files
Updates to: Controller Actions, Custom Scalars, Code Samples and typos (#16)
* updated docs for attributes, actions, scalars, controller actions, custom scalars
1 parent 29ad2f9 commit 22a8658

File tree

11 files changed

+120
-71
lines changed

11 files changed

+120
-71
lines changed

docs/advanced/custom-scalars.md

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Scalars are the most basic, fundamental unit of content in GraphQL. It is one of
88

99
Enums, being a type of their own, are very straight forward in .NET. Scalars, however; can be anything. For instance, the `Uri` scalar is represented in GraphQL by a string. On the server though, we convert it into a `System.Uri` object, with all the extra features that go along with it.
1010

11-
This can be done for any value that can be represented as a simple string of characters. When you create a scalar you declare its .NET type, provide a resolver that accepts raw data from a query (a `ReadOnlySpan<char>`) and returns the completed scalar value.
11+
This can be done for any value that can be represented as a simple set of characters. When you create a scalar you declare its .NET type, provide a value resolver that accepts raw data from a query (a `ReadOnlySpan<char>`) and returns the completed scalar value.
1212

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

@@ -58,7 +58,7 @@ query {
5858
</div>
5959
<br/>
6060

61-
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 graph type to resolve the value and generate the .NET object that can be passed to our action method.
61+
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.
6262

6363
## Implement IScalarGraphType
6464

@@ -77,13 +77,13 @@ namespace GraphQL.AspNet.Interfaces.TypeSystem
7777
ScalarValueType ValueType { get; }
7878
Type ObjectType { get; }
7979
TypeCollection OtherKnownTypes { get; }
80-
IScalarValueResolver SourceResolver { get; }
80+
ILeafValueResolver SourceResolver { get; }
8181
IScalarValueSerializer Serializer { get; }
8282

8383
bool ValidateObject(object item);
8484
}
8585

86-
public interface IScalarValueResolver
86+
public interface ILeafValueResolver
8787
{
8888
object Resolve(ReadOnlySpan<char> data);
8989
}
@@ -105,27 +105,27 @@ namespace GraphQL.AspNet.Interfaces.TypeSystem
105105
- `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`.
106106
- `ObjectType`: The primary, internal type representing the scalar in .NET. In our example above we would set this to `typeof(Money)`.
107107
- `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`).
108-
- `SourceResolver`: An object that implements `IScalarValueResolver` that converts raw input data into the scalar's primary `ObjectType`.
109-
- `Serializer`: An object that implements `IScalarValueSerializer` that converts the internal representation of the scalar (a class or struct) to a valid graphql scalar (a number, string or boolean).
108+
- `SourceResolver`: An object that implements `ILeafValueResolver` which can convert raw input data into the scalar's primary `ObjectType`.
109+
- `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).
110110
- `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.
111111

112112
> `ValidateObject(object)` should not attempt to enforce nullability rules. In general, all scalars should return `true` if the provided object is `null`.
113113
114-
### IScalarValueResolver Members
114+
### ILeafValueResolver Members
115115

116116
- `Resolve(ReadOnlySpan<char>)`: A resolver function capable of converting an array of characters into the internal representation of the type.
117117

118118
#### Dealing with Escaped Strings
119119

120-
The span provided to `IScalarValueResolver.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 unicode characters):
120+
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):
121121

122122
Example string data:
123123

124124
- `"quoted string"`
125125
- `"""triple quoted string"""`
126126
- `"With \"\u03A3scaped ch\u03B1racters\""`;
127127

128-
The `StringScalarType` provides a handy static method for converting the data if you don't need to do anything special with it, `StringScalarType.UnescapeAndTrimDelimiters`.
128+
The `StringScalarType` provides a handy static method for unescaping the data if you don't need to do anything special with it, `StringScalarType.UnescapeAndTrimDelimiters`.
129129

130130
Calling `UnescapeAndTrimDelimiters` with the previous examples produces:
131131

@@ -141,17 +141,17 @@ If you throw `UnresolvedValueException` your error message will be delivered ver
141141

142142
### IScalarValueSerializer Members
143143

144-
- `Serialize(object)`: A serializer that converts the internal representation of the scalar to a [graphql compliant scalar value](https://graphql.github.io/graphql-spec/June2018/#sec-Scalars); a `number`, `string`, `bool` or `null`.
144+
- `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`.
145145
- When converting to a number this can be any number value type (int, float, decimal etc.).
146146

147-
> `Serialize(object)` must return a valid graphql scalar type.
147+
> `Serialize(object)` must return a string, any primative number or a boolean.
148148
149149
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.
150150

151151
```csharp
152152
public class GuidScalarSerializer : IScalarValueSerializer
153153
{
154-
public override object Serialize(object item)
154+
public object Serialize(object item)
155155
{
156156
if (item == null)
157157
return item;
@@ -161,6 +161,8 @@ public class GuidScalarSerializer : IScalarValueSerializer
161161
}
162162
```
163163

164+
> The `Serialize()` method will only be given an object of the approved types for the scalar or null.
165+
164166
---
165167

166168
The Completed Money Scalar:
@@ -185,7 +187,7 @@ The Completed Money Scalar:
185187

186188
public TypeCollection OtherKnownTypes => TypeCollection.Empty;
187189

188-
public IScalarValueResolver SourceResolver { get; } = new MoneyScalarTypeResolver();
190+
public ILeafValueResolver SourceResolver { get; } = new MoneyLeafTypeResolver();
189191

190192
public IScalarValueSerializer Serializer { get; } = new MoneyScalarTypeSerializer()
191193

@@ -198,11 +200,12 @@ The Completed Money Scalar:
198200
}
199201
}
200202

201-
public class MoneyScalarTypeResolver : IScalarValueResolver
203+
public class MoneyLeafTypeResolver : ILeafValueResolver
202204
{
203205
public object Resolve(ReadOnlySpan<char> data)
204206
{
205-
// example only, more validation code is needed
207+
// example only, more validation code is needed to fully validate
208+
// the data
206209
var sanitizedMoney = StringScalarType.UnescapeAndTrimDelimiters(data);
207210
if(sanitizedMoney == null || sanitizedMoney.Length < 2)
208211
throw new UnresolvedValueException("Money must be at least 2 characters");
@@ -231,9 +234,9 @@ The last step in declaring a scalar is to register it with the runtime. Scalars
231234
// Startup.cs (other code)
232235
public void ConfigureServices(IServiceCollection services)
233236
{
234-
// register the scalar to the global provider
237+
// register the scalar type to the global provider
235238
// BEFORE a call to .AddGraphQL()
236-
GraphQLProviders.ScalarProvider.RegisterCustomScalar(new MoneyScalarType());
239+
GraphQLProviders.ScalarProvider.RegisterCustomScalar(typeof(MoneyScalarType));
237240

238241
services.AddMvc()
239242
.AddGraphQL();
@@ -242,22 +245,21 @@ public void ConfigureServices(IServiceCollection services)
242245

243246
Since our scalar is, internally, represented by a 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.
244247

245-
## When Developing a Scalar
248+
## Tips When Developing a Scalar
246249

247250
A few points about designing your scalar:
248251

249252
- Scalar types are expected to be thread safe.
253+
- 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.
250254
- Scalar types should be simple and work in isolation.
251255
- The `ReadOnlySpan<char>` provided to the `Resolve()` method should be all the data needed to generate a value, there should be no need to perform side effects or fetch additional data.
252-
- If you have a lot of logic to unpack a string to create your scalar consider using a regular object graph type instead.
253-
- A scalar type is a singleton instance.
254-
- Scalar types exist for the lifetime of the server instance.
255-
- Scalar types should not track any state or depend on any stateful objects.
256-
- `IScalarValueResolver.Resolve` must be **FAST**! Since your resolver is used to construct an initial query plan, it'll be called orders of magnitude more often than any controller action method.
256+
- If you have a lot of logic to unpack a string, consider using a regular OBJECT graph type instead.
257+
- Scalar types should not track any state or depend on any stateful objects.
258+
- `ILeafValueResolver.Resolve` must be **FAST**! Since your resolver is used to construct an initial query plan, it'll be called #orders of magnitude more often than any controller action method.
257259

258-
## Aim for Fewer Scalars
260+
### Aim for Fewer Scalars
259261

260-
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 object graph type:
262+
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:
261263

262264
<div class="sideBySideCode hljs">
263265
<div>

0 commit comments

Comments
 (0)