Add FluentValidation support to GraphQL.net
See Milestones for release notes.
https://nuget.org/packages/GraphQL.FluentValidation/
Given the following input:
publicclassMyInput{publicstringContent{get;set;}=null!;}And graph:
publicclassMyInputGraph:InputObjectGraphType{publicMyInputGraph()=>Field<StringGraphType>("content");}A custom validator can be defined as follows:
publicclassMyInputValidator:AbstractValidator<MyInput>{publicMyInputValidator()=>RuleFor(_ =>_.Content).NotEmpty();}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();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.
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);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:
Given a user context class of the following form:
publicclassMyUserContext(stringmyProperty):Dictionary<string,object?>{publicstringMyProperty{get;}=myProperty;}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);varoptions=newExecutionOptions{Schema=schema,Query=queryString,Variables=inputs,UserContext=newDictionary<string,object?>{{"MyUserContext",newMyUserContext(myProperty:"the value")}}};options.UseFluentValidation(validatorCache);If no instance is passed to ExecutionOptions.UserContext:
varoptions=newExecutionOptions{Schema=schema,Query=queryString,Variables=inputs};options.UseFluentValidation(validatorCache);Then the UseFluentValidation method will instantiate it to a new Dictionary<string, object>.
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};});}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");}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.
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.
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);}}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);}}Shield designed by Maxim Kulikov from The Noun Project
