Skip to content

Repository files navigation

GraphQL.Validation

Build statusNuGet Status

Add FluentValidation support to GraphQL.net

See Milestones for release notes.

Powered by

JetBrains logo.

NuGet package

https://nuget.org/packages/GraphQL.FluentValidation/

Usage

Define validators

Given the following input:

publicclassMyInput{publicstringContent{get;set;}=null!;}

snippet source | anchor

And graph:

publicclassMyInputGraph:InputObjectGraphType{publicMyInputGraph()=>Field<StringGraphType>("content");}

snippet source | anchor

A custom validator can be defined as follows:

publicclassMyInputValidator:AbstractValidator<MyInput>{publicMyInputValidator()=>RuleFor(_ =>_.Content).NotEmpty();}

snippet source | anchor

Setup Validators

Validators need to be added to the ValidatorTypeCache. This should be done once at application startup.

varvalidatorCache=newValidatorInstanceCache();validatorCache.AddValidatorsFromAssembly(assemblyContainingValidators);varschema=newSchema();schema.UseFluentValidation();varexecuter=newDocumentExecuter();

snippet source | anchor

Generally ValidatorTypeCache is scoped per app and can be collocated with Schema, DocumentExecuter initialization.

Dependency Injection can be used for validators. Create a ValidatorTypeCache with the useDependencyInjection: true parameter and call one of the AddValidatorsFrom* methods from FluentValidation.DependencyInjectionExtensions package in the Startup. By default, validators are added to the DI container with a transient lifetime.

Add to ExecutionOptions

Validation needs to be added to any instance of ExecutionOptions.

varoptions=newExecutionOptions{Schema=schema,Query=queryString,Variables=inputs};options.UseFluentValidation(validatorCache);varexecutionResult=awaitexecuter.ExecuteAsync(options);

snippet source | anchor

UserContext must be a dictionary

This library needs to be able to pass the list of validators, in the form of ValidatorTypeCache, through the graphql context. The only way of achieving this is to use the ExecutionOptions.UserContext. To facilitate this, the type passed to ExecutionOptions.UserContext has to implement IDictionary<string, object>. There are two approaches to achieving this:

1. Have the user context class implement IDictionary

Given a user context class of the following form:

publicclassMyUserContext(stringmyProperty):Dictionary<string,object?>{publicstringMyProperty{get;}=myProperty;}

snippet source | anchor

The ExecutionOptions.UserContext can then be set as follows:

varoptions=newExecutionOptions{Schema=schema,Query=queryString,Variables=inputs,UserContext=newMyUserContext(myProperty:"the value")};options.UseFluentValidation(validatorCache);

snippet source | anchor

2. Have the user context class exist inside a IDictionary

varoptions=newExecutionOptions{Schema=schema,Query=queryString,Variables=inputs,UserContext=newDictionary<string,object?>{{"MyUserContext",newMyUserContext(myProperty:"the value")}}};options.UseFluentValidation(validatorCache);

snippet source | anchor

No UserContext

If no instance is passed to ExecutionOptions.UserContext:

varoptions=newExecutionOptions{Schema=schema,Query=queryString,Variables=inputs};options.UseFluentValidation(validatorCache);

snippet source | anchor

Then the UseFluentValidation method will instantiate it to a new Dictionary<string, object>.

Trigger validation

To trigger the validation, when reading arguments use GetValidatedArgument instead of GetArgument:

publicclassQuery:ObjectGraphType{publicQuery()=>Field<ResultGraph>("inputQuery").Argument<MyInputGraph>("input").Resolve(context =>{varinput=context.GetValidatedArgument<MyInput>("input");returnnewResult{Data=input.Content};});}

snippet source | anchor

Accessing services in validators

When validation is triggered via GetValidatedArgument, the IResolveFieldContext.RequestServices for the current request is exposed to validators through the validation context. A rule can therefore resolve services — for example the current time, configuration, or a repository — from the request scope, which keeps the rule unit-testable:

publicclassBookingInputValidator:AbstractValidator<BookingInput>{publicBookingInputValidator()=>RuleFor(_ =>_.Start).Must((_,start,context)=>{varclock=context.GetRequiredService<TimeProvider>();returnstart>=clock.GetUtcNow();}).WithMessage("Start cannot be in the past");}

snippet source | anchor

Use context.GetRequiredService<T>() to resolve a required service. To access the underlying IServiceProvider directly, use context.GetServiceProvider(), or context.TryGetServiceProvider(out var provider) when validation may run without one.

Difference from IValidationRule

The validation implemented in this project has nothing to do with the validation of the incoming GraphQL request, which is described in the official specification. GraphQL.NET has a concept of validation rules that would work before request execution stage. In this project validation occurs for input arguments at the request execution stage. This additional validation complements but does not replace the standard set of validation rules.

Testing

Integration

A full end-to-en test can be run against the GraphQL controller:

publicclassGraphQLControllerTests{[Fact]publicasyncTaskRunQuery(){usingvarserver=GetTestServer();usingvarclient=server.CreateClient();varquery=""" { inputQuery(input: {content: "TheContent"}) { data } } """;varbody=new{query};varserialized=JsonConvert.SerializeObject(body);usingvarcontent=newStringContent(serialized,Encoding.UTF8,"application/json");usingvarrequest=newHttpRequestMessage(HttpMethod.Post,"graphql"){Content=content};usingvarresponse=awaitclient.SendAsync(request);awaitVerify(response);}staticTestServerGetTestServer(){varbuilder=newWebHostBuilder();builder.UseStartup<Startup>();returnnew(builder);}}

snippet source | anchor

Unit

Unit tests can be run a specific field of a query:

publicclassQueryTests{[Fact]publicasyncTaskRunInputQuery(){varfield=newQuery().GetField("inputQuery")!;varuserContext=newGraphQLUserContext();FluentValidationExtensions.AddCacheToContext(userContext,ValidatorCacheBuilder.Instance);varinput=newMyInput{Content="TheContent"};varfieldContext=newResolveFieldContext{Arguments=newDictionary<string,ArgumentValue>{{"input",new(input,ArgumentSource.Variable)}},UserContext=userContext};varresult=awaitfield.Resolver!.ResolveAsync(fieldContext);awaitVerify(result);}[Fact]publicTaskRunInvalidInputQuery(){Thread.CurrentThread.CurrentUICulture=new("en-US");varfield=newQuery().GetField("inputQuery")!;varuserContext=newGraphQLUserContext();FluentValidationExtensions.AddCacheToContext(userContext,ValidatorCacheBuilder.Instance);varinput=newMyInput{Content=null!};varfieldContext=newResolveFieldContext{Arguments=newDictionary<string,ArgumentValue>{{"input",new(input,ArgumentSource.Variable)}},UserContext=userContext};varexception=Assert.Throws<ValidationException>(()=>field.Resolver!.ResolveAsync(fieldContext));returnVerify(exception.Message);}}

snippet source | anchor

Icon

Shield designed by Maxim Kulikov from The Noun Project

About

Add FluentValidation support to GraphQL.net

Resources

Code of conduct

Stars

44 stars

Watchers

3 watching

Forks

Releases

Used by

Contributors

Languages