GraphQL - Introducing Input Validation in Mutations

  • 3 min read

Intro

I have been exploring GraphQL for almost a year now starting with the original GraphQL implementation written in Javascript and then moving on to the .NET version of it thereafter.

In today's post, I am going to cover validating certain input values (e.g. email address, password complexity, phone number format, etc.) as they are submitted in the form of mutation prior to executing the corresponding resolver.

The GraphQL.NET library itself comes bundled with a set of validation rules (one that you may have seen frequently is when an invalid query is executed and the server comes back with an error) but there are obviously a custom set of validation rules that you may wish to apply to fit your business requirement needs. The library comes with an interface called IValidationRule that you can inherit to write additional rules.

Let's create a simple email validation rule that will get executed when certain metadata exists.

Email Validation Rule

The first step is to inherit IValidationRule interface and then write an implementation of the Validate method. This particular method will look for arguments that are getting passed-in to the mutation. If it finds an argument that is of an Input Type then it will get all the fields that have a metadata (more on that later) and run validation rule on each.

See the example code below:


#region Namespaces

using System.Collections.Generic;
using System.Linq;
using GraphQL;
using GraphQL.Language.AST;
using GraphQL.Types;
using GraphQL.Validation;

#endregion

namespace TestApp.Types.Validators
{
    public class EmailAddressValidationRule : IValidationRule
    {
        public INodeVisitor Validate(ValidationContext context)
        {
            return new EnterLeaveListener(_ =>
            {
                _.Match<Argument>(argAst =>
                {
                    var argDef = context.TypeInfo.GetArgument();
                    if (argDef == null) return;

                    var type = argDef.ResolvedType;
                    if (type.IsInputType())
                    {
                        var fields = ((type as NonNullGraphType)?.ResolvedType as IComplexGraphType)?.Fields;
                        if (fields != null)
                        {
							//let's look for fields that have a specific metadata
                            foreach (var fieldType in fields.Where(f => f.HasMetadata(nameof(EmailAddressValidationRule))))
                            {
                                //now it's time to get the value
                                var value = context.Inputs.GetValue(argAst.Name, fieldType.Name);
                                if (value != null)
								{
									if (!value.IsValidEmail())
                                    {
                                        context.ReportError(new ValidationError(context.OriginalQuery
                                            , "Invalid Email Address"
                                            , "The supplied email address is not valid."
                                            , argAst
                                        ));
                                    }	
								}
                            }
                        }
                    }
                });
            });
        }
    }
	
	public static class GraphqlExtensions
	{
		/// <summary>
        /// A method that gets a value from a given input object with provided argument and field type name to match with.
        /// </summary>
        /// <param name="input">Provide an Inputs type.</param>
        /// <param name="argumentName">Provide name of the argument.</param>
        /// <param name="fieldTypeName">Provide field type name.</param>
        /// <returns>Returns a value from dictionary.</returns>
        public static string GetValue(this Inputs input, string argumentName, string fieldTypeName)
        {
            if (input.ContainsKey(argumentName))
            {
                var model = (Dictionary<string, object>)input[argumentName];
                if (model != null && model.ContainsKey(fieldTypeName))
                {
                    return model[fieldTypeName]?.ToString();
                }
            }

            return null;
        }
	}
	
	public static class StringExtensions
	{
		public static bool IsValidEmail(this string email) => Regex.IsMatch(email, 				@"^(([^<>()\[\]\.,;:\s@\""]+(\.[^<>()\[\]\.,;:\s@\""]+)*)|(\"".+\""))@(([^<>()[\]\.,;:\s@\""]+\.)+[^<>()[\]\.,;:\s@\""]{2,})$");
	}
}

The next step is to add this validation rule to a set of validation rules that already exist. The way to do that is to use DocumentValidator object to concatenate existing and new set of rule(s) as seen below.

public IndexController(IDocumentExecuter executer,
            IDocumentWriter writer, ISchema schema,
            IValidationRule validationRule)
{
    _executer = executer;
    _writer = writer;
    _schema = schema;
    _validationRules = DocumentValidator.CoreRules().Concat(new IValidationRule[]
    {
        //add additional validation rule(s) here.
        new EmailAddressValidationRule()
    });
}

Now that we have added our custom validation rule to an existing set of rules. All we need to do is add metadata to the field that is exposed from the Input Type.


Field(x => x.Email)
    .Description("Provide an email address of the member.")
    .Configure(type => type.Metadata.Add(nameof(EmailAddressValidationRule), null));

That's it. This will ensure that only the fields that have this metadata specified will be validated against.