Query Validation
There are a number of query validation rules
that are ran when a query is executed. All of these are turned on by default. You can add custom validation
rules by calling .AddValidationRule<T>()
within your DI configuration as follows:
services.AddGraphQL(b => b
.AddSchema<MySchema>()
.AddSystemTextJson()
.AddValidationRule<RequiresAuthValidationRule>());
When not using the DI builder methods, you can set the ExecutionOptions.ValidationRules
property when
calling IDocumentExecuter.ExecuteAsync
as follows:
await schema.ExecuteAsync(_ =>
{
_.Query = "...";
_.ValidationRules =
new[]
{
new RequiresAuthValidationRule()
}
.Concat(DocumentValidator.CoreRules);
});
Setting validation rules on input arguments or input object fields
When defining a schema, you can set validation rules on input arguments or input object fields.
This can be used to easily validate input values such as email addresses, phone numbers, or to
validate a value is within a specific range. The Validator
delegate is used to validate the
input value, and can be set via the Validate
method on the FieldBuilder
or QueryArgument
.
Here are some examples:
// for an input object graph type
Field(x => x.FirstName)
.Validate(value =>
{
if (((string)value).Length >= 10)
throw new ArgumentException("Length must be less than 10 characters.");
});
Field(x => x.Age)
.Validate(value =>
{
if ((int)value < 18)
throw new ArgumentException("Age must be 18 or older.");
});
Field(x => x.Password)
.Validate(value =>
{
VerifyPasswordComplexity((string)value);
});
The Validator
delegate is called during the validation stage, prior to execution of the request.
Null values are not passed to the validation function. Throwing an exception from the Validator
delegate will produce a response similar to the following:
{
"errors": [
{
"message": "Invalid value for argument 'firstName' of field 'testMe'. Length must be less than 10 characters.",
"locations": [
{
"line": 1,
"column": 14
}
],
"extensions": {
"code": "INVALID_VALUE",
"codes": [
"INVALID_VALUE",
"ARGUMENT"
],
"number": "5.6"
}
}
]
}
For additional control over the error message, you can throw a ValidationError
or derived
exception class with a custom error message. The locations
property will be automatically
set to the location of the input field or argument, but the message will remain the same.
For type-first schemas, you may define your own attributes to perform validation, either on input fields or on output field arguments. For example:
// for AutoRegisteringObjectGraphType<MyClass>
public class MyClass
{
public static string TestMe([MyMaxLength(5)] string value) => value;
}
private class MyMaxLength : GraphQLAttribute
{
private readonly int _maxLength;
public MyMaxLength(int maxLength)
{
_maxLength = maxLength;
}
public override void Modify(ArgumentInformation argumentInformation)
{
if (argumentInformation.TypeInformation.Type != typeof(string))
{
throw new InvalidOperationException("MyMaxLength can only be used on string arguments.");
}
}
public override void Modify(QueryArgument queryArgument)
{
queryArgument.Validate(value =>
{
if (((string)value).Length > _maxLength)
{
throw new ArgumentException($"Value is too long. Max length is {_maxLength}.");
}
});
}
}
When using the Validator
delegate, there is no need write or install a custom validation rule
to handle the validation. The Validator
delegate is called during the validation stage, and
will not unnecessarily trigger the unhandled exception handler due to client input errors.
At this time GraphQL.NET does not directly support the MaxLength
and similar attributes from
System.ComponentModel.DataAnnotations
, but this may be added in a future version. You can
implement your own attributes as shown above, or call the Validate
method to set a validation
function.
Custom Validation Rules
Validation rules are built with the IValidationRule
interface and can validate the document and variables
at different stages of the validation process: before parsing arguments, during parsing of variables, and
after parsing arguments. The INodeVisitor
interface is used to traverse the AST and report errors via
the ValidationContext
class, while the IVariableVisitor
interface is used to validate variables.
Also relevant is the TypeInfo
class, which provides type information for the current position in the document,
and the MatchingNodeVisitor
and NodeVisitors
helper classes for creating and combining node visitors.
IValidationRule
interface
IValidationRule
represents a validation rule for a GraphQL document. It consists of three methods:
GetPreNodeVisitorAsync
: Returns a node visitor for validating the document before parsing arguments.GetVariableVisitorAsync
: Returns a visitor used while parsing variables.GetPostNodeVisitorAsync
: Returns a node visitor for validating the document after parsing all arguments.
INodeVisitor
interface
INodeVisitor
handles events raised by a node walker. It has two methods:
EnterAsync(ASTNode node, ValidationContext context)
: Called when entering a node.LeaveAsync(ASTNode node, ValidationContext context)
: Called when leaving a node.
IVariableVisitor
interface
IVariableVisitor
is used to validate variables. It has four methods:
VisitScalarAsync
: Called when parsing a scalar value.VisitListAsync
: Called when parsing a list value.VisitObjectAsync
: Called when parsing an input object value.VisitFieldAsync
: Called when parsing a field of an input object value.
This interface mainly exists for legacy compatibility and is not recommended for new code. Rather, use the
Validate
method on FieldBuilder
or QueryArgument
to validate input values. See the schema node visitor
sample at the bottom of this document for a technique to apply validation rules across the entire schema.
For an example of a custom validation rule that implements GetVariableVisitorAsync
, see the
InputFieldsAndArgumentsOfCorrectLength
class.
ValidationRuleBase
class
ValidationRuleBase
is an abstract class that provides a default implementation of IValidationRule
. It can be
extended to create custom validation rules.
ValidationContext
class
ValidationContext
is used to report errors and track the state of the validation process.
Most of these mirror the same properties provided to the execution engine within ExecutionOptions
.
Other properties include the following:
Property | Description |
---|---|
Document |
The parsed document AST being validated. |
Operation |
The operation requested to be executed. |
TypeInfo |
The type information for the current position in the document. |
ArgumentValues |
Within GetPostNodeVisitorAsync , contains the parsed argument values for the document. |
DirectiveValues |
Within GetPostNodeVisitorAsync , contains the parsed directive values for the document. |
Method | Description |
---|---|
ReportError |
Reports an error found during validation. |
MatchingNodeVisitor
class
MatchingNodeVisitor
is a helper class that simplifies the process of creating node visitors.
The constructor takes a delegate that is called when a node matches the specified type.
The delegate can be synchronous or asynchronous, and a second delegate can be provided
to be called when leaving the node. See the examples below for usage.
NodeVisitors
class
NodeVisitors
is a helper class that provides methods for combining node visitors.
The constructor accepts an array of node visitors and calls them in order when entering and leaving nodes.
TypeInfo
class
TypeInfo
provides type information for the current node within the document. See xml comments
for exact details on each method.
Method | Description |
---|---|
GetAncestor |
Returns the ancestor of the current node. |
GetLastType |
Returns the last graph type matched. |
GetInputType |
Returns the last input graph type matched. |
GetParentType |
Returns the parent type of the current node. |
GetFieldDef |
Returns the field definition for the current node. |
GetDirective |
Returns the last directive matched. |
GetArgument |
Returns the last argument matched. |
Example 1: Disabling Introspection Requests
This rule prevents introspection requests.
/// <summary>
/// Analyzes the document for any introspection fields and reports an error if any are found.
/// </summary>
public class NoIntrospectionValidationRule : ValidationRuleBase, INodeVisitor
{
public override ValueTask<INodeVisitor?> GetPreNodeVisitorAsync(ValidationContext context) => new(this);
ValueTask INodeVisitor.EnterAsync(ASTNode node, ValidationContext context)
{
if (node is GraphQLField field)
{
if (field.Name.Value == "__schema" || field.Name.Value == "__type")
context.ReportError(new NoIntrospectionError(context.Document.Source, field));
}
return default;
}
ValueTask INodeVisitor.LeaveAsync(ASTNode node, ValidationContext context) => default;
}
Or when using the MatchingNodeVisitor
helper class:
public class NoIntrospectionValidationRule : ValidationRuleBase
{
private static readonly MatchingNodeVisitor<GraphQLField> _visitor = new(
(field, context) =>
{
if (field.Name.Value == "__schema" || field.Name.Value == "__type")
context.ReportError(new NoIntrospectionError(context.Document.Source, field));
});
public override ValueTask<INodeVisitor?> GetPreNodeVisitorAsync(ValidationContext context) => new(_visitor);
}
Example 2: Limiting Connections to Under 1000 Rows
This rule limits the number of rows returned in a connection to 1000.
services.AddGraphQL(b => b
.AddSchema<MySchema>()
.AddValidationRule<NoConnectionOver1000ValidationRule>());
public class NoConnectionOver1000ValidationRule : ValidationRuleBase, IVariableVisitorProvider, INodeVisitor
{
public override ValueTask<INodeVisitor?> GetPostNodeVisitorAsync(ValidationContext context)
=> context.ArgumentValues != null ? new(this) : default;
ValueTask INodeVisitor.EnterAsync(ASTNode node, ValidationContext context)
{
if (node is not GraphQLField fieldNode)
return default;
var fieldDef = context.TypeInfo.GetFieldDef();
if (fieldDef == null || fieldDef.ResolvedType?.GetNamedType() is not IObjectGraphType connectionType || !connectionType.Name.EndsWith("Connection"))
return default;
if (!(context.ArgumentValues?.TryGetValue(fieldNode, out var args) ?? false))
return default;
ArgumentValue lastArg = default;
if (!args.TryGetValue("first", out var firstArg) && !args.TryGetValue("last", out lastArg))
return default;
var rows = (int?)firstArg.Value ?? (int?)lastArg.Value ?? 0;
if (rows > 1000)
context.ReportError(new ValidationError("Cannot return more than 1000 rows"));
return default;
}
ValueTask INodeVisitor.LeaveAsync(ASTNode node, ValidationContext context) => default;
}
Adding validation rules via schema node visitor
You may also add validation rules via a schema node visitor. The below sample performs the same validation as the previous example, but uses a schema node visitor. The schema node visitor is called when the schema is built, and adds the appropriate validation rules to the schema.
services.AddGraphQL(b => b
.AddSchema<MySchema>()
.AddSchemaVisitor<NoConnectionOver1000Visitor>());
public class NoConnectionOver1000Visitor : BaseSchemaNodeVisitor
{
public override void VisitObjectFieldArgumentDefinition(QueryArgument argument, FieldType field, IObjectGraphType type, ISchema schema)
=> argument.Validator += GetValidator(argument, field);
public override void VisitInterfaceFieldArgumentDefinition(QueryArgument argument, FieldType field, IInterfaceGraphType type, ISchema schema)
=> field.Validator += GetValidator(argument, field);
private static Action<object?>? GetValidator(QueryArgument argument, FieldType field)
{
// identify fields that return a connection type
if (!field.ResolvedType!.GetNamedType().Name.EndsWith("Connection"))
return null;
// identify the first and last arguments
if (argument.Name != "first" && argument.Name != "last")
return null;
// apply the validation rule
return value =>
{
if (value is int intValue && intValue > 1000)
throw new ArgumentException("Cannot return more than 1000 rows.");
};
}
}
With the visitor approach, the validation is only evaluated at runtime when the applicable field is requested, with no performance penalty otherwise. It is also considerably simpler to implement.