A GraphQL schema should be built as expressive as possible.
Just from looking at the schema, a developer should know how to use the API.
In GraphQL you are not limited to only describing the structure of a type, you can even describe value types.
Scalar types represent types that can hold data of a specific kind.
Scalars are leaf types, meaning you cannot use e.g. { fieldname }
to further drill down into the type.
A scalar must only know how to serialize and deserialize the value of the field.
GraphQL gives you the freedom to define custom scalar types.
This makes them the perfect tool for expressive value types.
You could create a scalar for CreditCardNumber
or NonEmptyString
.
The GraphQL specification defines the following scalars
Type | Description |
---|---|
Int |
Signed 32-bit numeric non-fractional value |
Float |
Double-precision fractional values as specified by IEEE 754 |
String |
UTF-8 character sequences |
Boolean |
Boolean type representing true or false |
ID |
Unique identifier |
In addition to the scalars defined by the specification, HotChocolate also supports the following set of scalar types:
Type | Description |
---|---|
Byte |
|
ByteArray |
Base64 encoded array of bytes |
Short |
Signed 16-bit numeric non-fractional value |
Long |
Signed 64-bit numeric non-fractional value |
Decimal |
.NET Floating Point Type |
Url |
Url |
DateTime |
ISO-8601 date time |
Date |
ISO-8601 date |
Uuid |
GUID |
Any |
This type can be anything, string, int, list or object etc. |
Using Scalars
HotChocolate will automatically detect which scalars are in use and will only expose those in the introspection. This keeps the schema definition small, simple and clean.
The schema discovers .NET types and binds the matching scalar to the type.
HotChocolate, for example, automatically binds the StringType
on a member of the type System.String
.
You can override these mappings by explicitly specifying type bindings on the request executor builder.
public void ConfigureServices(IServiceCollection services)
{
services
.AddRouting()
.AddGraphQLServer()
.BindRuntimeType<string, StringType>()
.AddQueryType<Query>();
}
Furthermore, you can also bind scalars to arrays or type structures:
public void ConfigureServices(IServiceCollection services)
{
services
.AddRouting()
.AddGraphQLServer()
.BindRuntimeType<byte[], ByteArrayType>()
.AddQueryType<Query>();
}
Any Type
The Any
scalar is a special type that can be compared to object
in C#.
Any
allows us to specify any literal or return any output type.
Consider the following type:
type Query {
foo(bar: Any): String
}
Since our field foo
specifies an argument bar
of type Any
all of the following queries would be valid:
{
a: foo(bar: 1)
b: foo(bar: [1, 2, 3, 4, 5])
a: foo(bar: "abcdef")
a: foo(bar: true)
a: foo(bar: { a: "foo", b: { c: 1 } })
a: foo(bar: [{ a: "foo", b: { c: 1 } }, { a: "foo", b: { c: 1 } }])
}
The same goes for the output side. Any
can return a structure of data although it is a scalar type.
If you want to access the data you can either fetch data as an object or you can ask the context to provide it as a specific object.
Foo foo = context.Argument<Foo>("bar");
We can also ask the context which kind the current argument is:
ValueKind kind = context.ArgumentKind("bar");
The value kind will tell us by which kind of literal the argument is represented.
An integer literal can still contain a long value and a float literal could be a decimal but it also could just be a float.
public enum ValueKind
{
String,
Integer,
Float,
Boolean,
Enum,
Object,
Null
}
If you want to access an object dynamically without serializing it to a strongly typed model you can get it as IReadOnlyDictionary<string, object>
or as ObjectValueNode
.
Lists can be accessed generically by getting them as IReadOnlyList<object>
or as ListValueNode
.
Custom Converter
HotChocolate converts .Net types to match the types supported by the scalar of the field.
By default, all standard .Net types have converters registered.
You can register converters and reuse the built-in scalar types.
In case you use a non-standard library, e.g. NodeTime, you can register a converter and use the standard DateTimeType
.
public class Query
{
public OffsetDateTime GetDateTime(OffsetDateTime offsetDateTime)
{
return offsetDateTime;
}
}
Startup
public void ConfigureServices(IServiceCollection services)
{
services
.AddGraphQLServer()
.AddQueryType<Query>()
.BindRuntimeType<OffsetDateTime, DateTimeType>()
.AddTypeConverter<OffsetDateTime, DateTimeOffset>(
x => x.ToDateTimeOffset())
.AddTypeConverter<DateTimeOffset, OffsetDateTime>(
x => OffsetDateTime.FromDateTimeOffset(x));
}
Custom Scalars
All scalars in HotChocolate are defined though a ScalarType
The easiest way to create a custom scalar is to extend ScalarType<TRuntimeType, TLiteral>
.
This base class already includes basic serialization and parsing logic.
public sealed class CreditCardNumberType
: ScalarType<string, StringValueNode>
{
private readonly ICreditCardValidator _validator;
/// Like all type system objects, Scalars have support for dependency injection
public CreditCardNumberType(ICreditCardValidator validator)
: base("CreditCardNumber")
{
_validator = validator;
Description = "Represents a credit card number in the format of XXXX XXXX XXXX XXXX";
}
/// <summary>
/// Checks if a incoming StringValueNode is valid. In this case the string value is only
/// valid if it passes the credit card validation
/// </summary>
/// <param name="valueSyntax">The valueSyntax to validate</param>
/// <returns>true if the value syntax holds a valid credit card number</returns>
protected override bool IsInstanceOfType(StringValueNode valueSyntax)
{
return _validator.ValidateCreditCard(valueSyntax.Value);
}
/// <summary>
/// Checks if a incoming string is valid. In this case the string value is only
/// valid if it passes the credit card validation
/// </summary>
/// <param name="runtimeValue">The valueSyntax to validate</param>
/// <returns>true if the value syntax holds a valid credit card number</returns>
protected override bool IsInstanceOfType(string runtimeValue)
{
return _validator.ValidateCreditCard(runtimeValue);
}
/// <summary>
/// Converts a StringValueNode to a string
/// </summary>
protected override string ParseLiteral(StringValueNode valueSyntax) =>
valueSyntax.Value;
/// <summary>
/// Converts a string to a StringValueNode
/// </summary>
protected override StringValueNode ParseValue(string runtimeValue) =>
new StringValueNode(runtimeValue);
/// <summary>
/// Parses a result value of this into a GraphQL value syntax representation.
/// In this case this is just ParseValue
/// </summary>
public override IValueNode ParseResult(object? resultValue) =>
ParseValue(resultValue);
}
By extending ScalarType
you have full control over serialization and parsing.
public sealed class CreditCardNumberType
: ScalarType
{
private readonly ICreditCardValidator _validator;
/// Like all type system objects, Scalars have support for dependency injection
public CreditCardNumberType(ICreditCardValidator validator)
: base("CreditCardNumber")
{
_validator = validator;
Description = "Represents a credit card number in the format of XXXX XXXX XXXX XXXX";
}
// define which .NET type represents your type
public override Type RuntimeType { get; } = typeof(string);
// define which literals this type can be parsed from.
public override bool IsInstanceOfType(IValueNode valueSyntax)
{
if (valueSyntax == null)
{
throw new ArgumentNullException(nameof(valueSyntax));
}
return valueSyntax is StringValueNode stringValueNode &&
_validator.ValidateCreditCard(stringValueNode.Value);
}
// define how a literal is parsed to the native .NET type.
public override object ParseLiteral(IValueNode valueSyntax, bool withDefaults = true)
{
if (valueSyntax is StringValueNode stringLiteral &&
_validator.ValidateCreditCard(stringLiteral.Value))
{
return stringLiteral.Value;
}
throw new SerializationException(
"The specified value has to be a credit card number in the format " +
"XXXX XXXX XXXX XXXX",
nameof(valueSyntax));
}
// define how a native type is parsed into a literal,
public override IValueNode ParseValue(object? runtimeValue)
{
if (runtimeValue is string s &&
_validator.ValidateCreditCard(s))
{
return new StringValueNode(null, s, false);
}
throw new SerializationException(
"The specified value has to be a credit card number in the format " +
"XXXX XXXX XXXX XXXX");
}
public override IValueNode ParseResult(object? resultValue)
{
if (resultValue is string s &&
_validator.ValidateCreditCard(s))
{
return new StringValueNode(null, s, false);
}
throw new SerializationException(
"The specified value has to be a credit card number in the format " +
"XXXX XXXX XXXX XXXX");
}
public override bool TrySerialize(object? runtimeValue, out object? resultValue)
{
if (runtimeValue is string s &&
_validator.ValidateCreditCard(s))
{
resultValue = s;
return true;
}
resultValue = null;
return false;
}
public override bool TryDeserialize(object? serialized, out object? value)
{
if (serialized is string s &&
_validator.ValidateCreditCard(s))
{
value = s;
return true;
}
value = null;
return false;
}
}