You can extend your schema with your own custom scalars. Conceptually, a scalar must implement the following operations:
Serialization: Transforms a scalar from its server-side representation to a representation suitable for the client.
Value Parsing: Transforms a scalar from its client-side representation as a variable to its server-side representation.
Literal Parsing: Transforms a scalar from its client-side representation as an argument to its server-side representation.
Parsing for arguments and variables are handled separately because while arguments must always be expressed in GraphQL query syntax, variable format is transport-specific (usually JSON). You can find more information about these methods here.
You may wish to read more about how scalars work at the following links:
Vector3 sample with string parsing and serializationThe following example shows how to create a custom scalar in GraphQL.NET. You will create a 3D Vector which will be exchanged between server and client as a comma-separated string (ex. "34, 61, 12"). The example assumes the GraphQL schema is implemented in an ASP.NET Core project using the Microsoft.Extensions.DependencyInjection
package, though only minor modifications would be required for other project types.
Assume the following schema
scalar Vector3
schema {
query: {
getVector: Vector3!
}
mutation: {
addVector(vector3: Vector3!): Vector3
}
}
The goal is to execute mutations with both arguments:
mutation {
addVector(vector3: "23,43,66")
}
and also variables:
mutation AddVector($vector3: Vector3!) {
addVector(vector3: $vector3)
}
//variables
{
"vector3": "23,43,66"
}
Vectors should be returned in the same format:
{
"data": {
"getVector": "23,43,66"
}
}
1. Create the class/struct for the server-side representation.
This is not necessary if you intend to use already existing classes (string
, int
, etc.) for the server-side representation.
public struct Vector3
{
public Vector3(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
}
2. Create a graph type for the scalar by inheriting ScalarGraphType
.
using GraphQL;
using GraphQL.Types;
using GraphQLParser.AST;
public class Vector3Type : ScalarGraphType
{
public Vector3Type()
{
Name = "Vector3";
}
public override object? ParseLiteral(GraphQLValue value)
{
throw new NotImplementedException();
}
public override object? ParseValue(object? value)
{
throw new NotImplementedException();
}
public override object? Serialize(object? value)
{
throw new NotImplementedException();
}
}
3. Register the graph type with the DI container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<Vector3Type>();
}
4. Prepare to accept Vector3
inputs from query arguments. Implement ScalarGraphType.ParseLiteral
.
Keep in mind that AST parsing may present values as any of the following types:
GraphQLIntValue
- integer valuesGraphQLFloatValue
- floating-point values that can be represented within a double
GraphQLStringValue
- string valuesGraphQLEnumValue
- enumeration valuesGraphQLBooleanValue
- boolean values - true
and false
GraphQLNullValue
- representing null
- must be handled by all scalarsGraphQLObjectValue
- complex values with nested levelsGraphQLListValue
- lists of valuesIf your custom scalar accepts floating-point values, you must be sure to handle GraphQLIntValue
as well, since queries like { field(arg: 3) }
is parsed as an GraphQLIntValue
even though it is also a valid floating-point number.
In the sample below, only GraphQLNullValue
and GraphQLStringValue
need to be handled.
For any type that is not handled, or when the value cannot be parsed, you must throw an exception. ThrowLiteralConversionError
is provided as a convenient method to facilitate throwing an exception when the type does not match.
public override object? ParseLiteral(GraphQLValue value)
{
if (value is GraphQLNullValue)
return null;
if (value is GraphQLStringValue stringValue)
return ParseValue((string)stringValue.Value);
return ThrowLiteralConversionError(value);
}
Once the raw string is extracted from the value node, normal parsing can proceed.
5. Prepare to acceptVector3
inputs from query variables. Implement ScalarGraphType.ParseValue
.
Similar to ParseLiteral
, you must keep in mind the expected format of values that are likely to be presented to this method. For instance, if you are using a JSON deserializer, you may be presented with values of any of these types (or more, depending on your deserializer configuration):
int
long
ulong
BigInteger
double
decimal
string
null
- must be handled by all scalarsOn top of that, if you are calling this method from ParseLiteral
, you must handle the types passed from it. So if your scalar needs to handle floating-point values, you likely need to handle int
, long
, ulong
, BigInteger
, double
and decimal
types.
For any type that is not handled, or when the value cannot be parsed, you must throw an exception. ThrowValueConversionError
is provided as a convenient method to facilitate throwing an exception when the type does not match.
In the sample below, only null
and string
types need to be handled.
public override object? ParseValue(object? value)
{
if (value == null)
return null;
if (value is string vector3InputString)
{
try
{
var vector3Parts = vector3InputString.Split(',');
var x = float.Parse(vector3Parts[0]);
var y = float.Parse(vector3Parts[1]);
var z = float.Parse(vector3Parts[2]);
return new Vector3(x, y, z);
}
catch
{
throw new FormatException($"Failed to parse {nameof(Vector3)} from input '{vector3InputString}'. Input should be a string of three comma-separated floats in X Y Z order, ex. 1.0,2.0,3.0");
}
}
return ThrowValueConversionError(value);
}
6. Implement ScalarGraphType.Serialize
so Vector3
instances can be sent to the client.
Keep in mind that a null
value also required to be handled.
It is recommended that the type of data this method returns match the same type as is handled within ParseValue
. In this case, serialization of Vector3
is serialized as a string, which matches the expected data type within ParseValue
.
public override object? Serialize(object? value)
{
if (value == null)
return null;
if (value is Vector3 vector3)
{
return $"{vector3.X},{vector3.Y},{vector3.Z}";
}
return ThrowSerializationError(value);
}
7. Override ScalarGraphType.ToAST
if necessary.
The infrastructure converts default field values to AST representations during initialization in order to verify that the default values are valid within an AST tree. The default implementation calls Serialize
to convert the value to its client-side equivalent, then embeds it into an appropriate AST node based on the data type returned from Serialize
. It is likely you will only need to override this method if you are creating a custom scalar that returns enumeration values, or if you are returning structured data.
In order to allow simple field mappings like Field(x => x.Vector)
, you need to register the custom scalar within the schema so that the proper graph type can be selected for the field. Call RegisterTypeMapping
with the CLR data model type and the graph type you want it mapped to.
public class MySchema
{
public MySchema()
{
...
this.RegisterTypeMapping<Vector3, Vector3Type>();
}
}
This is not necessary if you use the alternate Field<T>
syntax which specifies the graph type to be used for the field, or if your scalar data type is marked with the [InputType]
and/or [OutputType]
attributes.
In this example, you created a custom scalar. In summary:
ScalarGraphType
which handles variable parsing, literal parsing, and serializationScalarGraphType
within the DI containerYou can also choose to override CanParseLiteral
, CanParseValue
or IsValidDefault
for enhanced performance. The default implementations call ParseLiteral
, ParseValue
and ToAST
respectively, returning false
if an exception is caught, or true
otherwise. If you do choose to implement these methods, note that those methods must not throw an exception, and that they are not always called when executing a document.
Keep in mind that the serialized value returned by custom scalar can be anything that the environment allows. For example it can be a structured object, rather than a simple value.
So to extend our sample, let's assume that we want the Vector3 scalar to instead accept and return data in a more structured format, in addition to supporting the string format for literals.
Here is a sample of a variable supporting a more structured format:
mutation AddVector($vector3: Vector3!) {
addVector(vector3: $vector3)
}
//variables
{
"vector3": {
"x":"23",
"y":"43",
"z":"66"
}
}
And a sample of a response with a vector in a more structured format:
{
"data": {
"getVector": {
"x":"23",
"y":"43",
"z":"66"
}
}
}
1. Change ParseLiteral
to accept strings or structured data:
public override object? ParseLiteral(GraphQLValue value)
{
if (value is GraphQLNullValue)
return null;
if (value is GraphQLStringValue stringValue)
return ParseValue((string)stringValue.Value);
if (value is GraphQLObjectValue objectValue)
{
var entries = objectValue.Fields.ToDictionary(x => x.Name.Value, x => _floatScalar.ParseLiteral(x.Value));
if (entries.Count != 3)
return ThrowLiteralConversionError(value);
var x = (double)entries["x"];
var y = (double)entries["y"];
var z = (double)entries["z"];
return new Vector3((float)x, (float)y, (float)z);
}
return ThrowLiteralConversionError(value);
}
2. Change ParseValue
to accept strings or structured data:
public override object? ParseValue(object? value)
{
if (value == null)
return null;
if (value is string vector3InputString)
{
try
{
var vector3Parts = vector3InputString.Split(',');
var x = float.Parse(vector3Parts[0]);
var y = float.Parse(vector3Parts[1]);
var z = float.Parse(vector3Parts[2]);
return new Vector3(x, y, z);
}
catch
{
throw new FormatException($"Failed to parse {nameof(Vector3)} from input '{vector3InputString}'. Input should be a string of three comma-separated floats in X Y Z order, ex. 1.0,2.0,3.0");
}
}
if (value is IDictionary<string, object> dictionary)
{
try
{
var x = Convert.ToSingle(dictionary["x"]);
var y = Convert.ToSingle(dictionary["y"]);
var z = Convert.ToSingle(dictionary["z"]);
if (dictionary.Count > 3)
return ThrowValueConversionError(value);
return new Vector3(x, y, z);
}
catch
{
throw new FormatException($"Failed to parse {nameof(Vector3)} from object. Input should be an object of three floats named X Y and Z");
}
}
return ThrowValueConversionError(value);
}
3. Change Serialize
to return structured data.
public override object? Serialize(object? value)
{
if (value == null)
return null;
if (value is Vector3 vector3)
{
return new
{
x = vector3.X,
y = vector3.Y,
z = vector3.Z
};
}
return ThrowSerializationError(value);
}
4. Change ToAST
to return an AST literal that represents the data.
Since Serialize
no longer returns a type that can be converted to an AST node, it is necessary to override this method.
public override GraphQLValue ToAST(object? value)
{
if (value == null)
return new GraphQLNullValue();
if (value is Vector3 vector3)
{
return new GraphQLObjectValue
{
Fields = new List<GraphQLObjectField>
{
new GraphQLObjectField
{
Name = new GraphQLName("x"),
Value = new GraphQLFloatValue(vector3.X))
},
new GraphQLObjectField
{
Name = new GraphQLName("y"),
Value = new GraphQLFloatValue(vector3.Y))
},
new GraphQLObjectField
{
Name = new GraphQLName("z"),
Value = new GraphQLFloatValue(vector3.Z))
}
}
};
}
return ThrowASTConversionError(value);
}
With these changes, a literal or variable can be parsed as a string or as a structured object, and is always returned as a structured object.
ValueConverterWhen GetArgument<T>
is called, the argument value is coerced to the requested type via the ValueConverter
. No conversion takes place when the requested type matches the type of the object or scalar (the type returned from ParseLiteral
or ParseValue
). But you can also use the value converter to assist with input deserialization.
For instance, you may be using IdGraphType
within your schema as unique identifiers for your data objects. Pursuant to the GraphQL specification, these identifiers may be passed as strings such as in the below example:
{
widget (id: "3") {
name
}
}
However, your code may use integer identifiers in the data access layer. So when you call context.GetArgument<int>("id")
, GraphQL.NET calls the value converter to convert the string to an integer.
The value converter can be extended globally by calling the static method Register
as follows:
ValueConverter.Register<Vector3, string>(v => $"{v.X},{v.Y},{v.Z}");
The above method registers a conversion from the Vector3
struct to a string
. Since the registration is static, it should only be done once per application lifetime. For instance, in a static constructor of your schema.
public class MySchema : Schema
{
static MySchema()
{
ValueConverter.Register<Vector3, string>(v => $"{v.X},{v.Y},{v.Z}");
}
...
}
Null values
Custom scalars process and handle null values during serialization and deserialization. For instance, let's say you have a database where database ID values of 0 should be represented as GraphQL 'null' values. You could of course write code within the field resolver to translate the values, or you may utilize a custom scalar to translate the values for you. The below example of a custom scalar does this and also rejects external values that are not strings containing integers, and rejects 0 or negative values:
public class DbIdGraphType : ScalarGraphType
{
public DbIdGraphType()
{
Name = "DbId";
}
public override object? ParseLiteral(GraphQLValue value) => value switch
{
GraphQLStringValue s => int.TryParse(s.Value, out int i) && i > 0 ? i : throw new FormatException($"'{s.Value}' is not a valid identifier."),
GraphQLNullValue _ => 0,
_ => ThrowLiteralConversionError(value)
};
public override object? ParseValue(object? value) => value switch
{
string s => int.TryParse(s, out int i) && i > 0 ? i : throw new FormatException($"'{s}' is not a valid identifier."),
null => 0,
_ => ThrowValueConversionError(value)
};
public override object? Serialize(object? value) => value switch
{
int i => i > 0 ? i.ToString() : i == 0 ? null : ThrowSerializationError(value),
_ => ThrowSerializationError(value)
};
}
Replacing built-in scalar types
In some cases you may want or need to replace the functionality of the built-in graph types. This can be accomplished by registering a replacement scalar before the schema has been initialized. Keep in mind that replacing a built-in type may affect the operation of introspection queries.
In order to replace a built-in scalar graph type, the new scalar graph type must:
Name
property set to the name of the built-in graph type.You may then override any of the members to provide custom implementations. Note that most of the built-in scalars override CanParseLiteral
, so it may be necessary to override that method if you override ParseLiteral
. Check the source code for the built-in scalar type you are overriding for further reference.
Below is a sample of how to replace the built-in BooleanGraphType
so it will accept 0 and non-zero values to represent false
and true
.
MyBooleanGraphType
Inherit from BooleanGraphType
and set the name to be Boolean
.
public class MyBooleanGraphType : BooleanGraphType
{
public MyBooleanGraphType()
{
Name = "Boolean";
}
}
2. Override the methods as necessary
In this case we must override all of them except Serialize
and IsValidDefault
.
public class MyBooleanGraphType : BooleanGraphType
{
public MyBooleanGraphType()
{
Name = "Boolean";
}
public override object? ParseLiteral(GraphQLValue value) => value switch
{
GraphQLBooleanValue b => b.BoolValue,
GraphQLIntValue i => ParseValue(ParseDoubleAccordingSpec(i)),
GraphQLFloatValue f => ParseValue(ParseDoubleAccordingSpec(f)),
GraphQLStringValue s => ParseValue((string)s.Value),
GraphQLNullValue _ => null,
_ => ThrowLiteralConversionError(value)
}
public override bool CanParseLiteral(GraphQLValue value)
{
try
{
_ = ParseLiteral(value);
return true;
}
catch
{
return false;
}
}
public override object? ParseValue(object? value) => value switch
{
bool _ => value,
byte b => b != 0,
sbyte sb => sb != 0,
short s => s != 0,
ushort us => us != 0,
int i => i != 0,
uint ui => ui != 0,
long l => l != 0,
ulong ul => ul != 0,
BigInteger bi => bi != 0,
float f => f != 0,
double d => d != 0,
decimal d => d != 0,
string s => bool.Parse(s),
null => null,
_ => ThrowValueConversionError(value)
}
public override bool CanParseValue(object? value)
{
try
{
_ = ParseValue(value);
return true;
}
catch
{
return false;
}
}
public override GraphQLValue ToAST(object? value) => Serialize(value) switch
{
bool b => new GraphQLBooleanValue(b),
null => new GraphQLNullValue(),
_ => ThrowASTConversionError(value)
};
}
3. Register the custom scalar within your schema.
The final step is to register an instance of the custom scalar within the schema. This can be done for code-first or schema-first schemas. For code-first schemas, register it within your constructor via RegisterType
, as follows:
public class MySchema : Schema
{
public void MySchema()
{
Query = ....;
RegisterType(new MyBooleanGraphType());
}
}
Alternatively, you may register the type within your DI engine.
services.AddSingleton<BooleanGraphType, MyBooleanGraphType>();
For schema-first schemas, register it immediately after calling Schema.For
to create the schema. Immediately after calling Schema.For
the schema is not yet initialized, therefore allowing registration of types.
var schema = Schema.For(...);
schema.RegisterType(new MyBooleanGraphType());
Now all BooleanGraphType
references in your schema will utilize the new MyBooleanGraphType
registered within the schema. This technique can be used to replace any of the built-in graph types.
Note that if you set the ResolvedType
property of a field or argument to an instance of a built-in type, or provide an instance of a built-in type to an applicable constructor, it will not be replaced with your registered replacement built-in type. For example, consider this code:
Field<StringGraphType>("sample")
.Argument<BooleanGraphType>("argNewBehavior")
.Arguments(new QueryArgument(new BooleanGraphType()) { Name = "argOldBehavior" })
.Resolve(...);
This is by design. However, you can call the ReplaceScalar
extension method after the schema is built, which will walk through all the graph types configured on the schema and replace any remaining references of the "Boolean" scalar graph type with references to your replacement scalar graph type. The scalar to be replaced is matched based on the name of the scalar.
var schema = Schema.For(...);
schema.ReplaceScalar(new MyBooleanGraphType());
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4