GraphQL.Validation
GraphQL.Validation copied to clipboard
Add FluentValidation support to GraphQL.net
GraphQL.Validation
Add FluentValidation support to GraphQL.net
NuGet package
https://nuget.org/packages/GraphQL.FluentValidation/
Usage
Define validators
Given the following input:
public class MyInput
{
public string Content { get; set; } = null!;
}
snippet source | anchor
And graph:
public class MyInputGraph :
InputObjectGraphType
{
public MyInputGraph() =>
Field<StringGraphType>("content");
}
snippet source | anchor
A custom validator can be defined as follows:
public class MyInputValidator :
AbstractValidator<MyInput>
{
public MyInputValidator() =>
RuleFor(_ => _.Content)
.NotEmpty();
}
snippet source | anchor
Setup Validators
Validators need to be added to the ValidatorTypeCache. This should be done once at application startup.
var validatorCache = new ValidatorInstanceCache();
validatorCache.AddValidatorsFromAssembly(assemblyContainingValidators);
var schema = new Schema();
schema.UseFluentValidation();
var executer = new DocumentExecuter();
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.
var options = new ExecutionOptions
{
Schema = schema,
Query = queryString,
Variables = inputs
};
options.UseFluentValidation(validatorCache);
var executionResult = await executer.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:
public class MyUserContext :
Dictionary<string, object?>
{
public MyUserContext(string myProperty) =>
MyProperty = myProperty;
public string MyProperty { get; }
}
snippet source | anchor
The ExecutionOptions.UserContext can then be set as follows:
var options = new ExecutionOptions
{
Schema = schema,
Query = queryString,
Variables = inputs,
UserContext = new MyUserContext
(
myProperty: "the value"
)
};
options.UseFluentValidation(validatorCache);
snippet source | anchor
2. Have the user context class exist inside a IDictionary
var options = new ExecutionOptions
{
Schema = schema,
Query = queryString,
Variables = inputs,
UserContext = new Dictionary<string, object?>
{
{
"MyUserContext",
new MyUserContext
(
myProperty: "the value"
)
}
}
};
options.UseFluentValidation(validatorCache);
snippet source | anchor
No UserContext
If no instance is passed to ExecutionOptions.UserContext:
var options = new ExecutionOptions
{
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:
public class Query :
ObjectGraphType
{
public Query() =>
Field<ResultGraph>("inputQuery")
.Argument<MyInputGraph>("input")
.Resolve(context =>
{
var input = context.GetValidatedArgument<MyInput>("input");
return new Result
{
Data = input.Content
};
}
);
}
snippet source | anchor
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:
[UsesVerify]
public class GraphQLControllerTests
{
[Fact]
public async Task RunQuery()
{
using var server = GetTestServer();
using var client = server.CreateClient();
var query = @"
{
inputQuery(input: {content: ""TheContent""}) {
data
}
}
";
var body = new
{
query
};
var serialized = JsonConvert.SerializeObject(body);
using var content = new StringContent(
serialized,
Encoding.UTF8,
"application/json");
using var request = new HttpRequestMessage(HttpMethod.Post, "graphql")
{
Content = content
};
using var response = await client.SendAsync(request);
await Verify(response);
}
static TestServer GetTestServer()
{
var builder = new WebHostBuilder();
builder.UseStartup<Startup>();
return new(builder);
}
}
snippet source | anchor
Unit
Unit tests can be run a specific field of a query:
[UsesVerify]
public class QueryTests
{
[Fact]
public async Task RunInputQuery()
{
var field = new Query().GetField("inputQuery")!;
var userContext = new GraphQLUserContext();
FluentValidationExtensions.AddCacheToContext(
userContext,
ValidatorCacheBuilder.Instance);
var input = new MyInput
{
Content = "TheContent"
};
var fieldContext = new ResolveFieldContext
{
Arguments = new Dictionary<string, ArgumentValue>
{
{
"input", new ArgumentValue(input, ArgumentSource.Variable)
}
},
UserContext = userContext
};
var result = await field.Resolver!.ResolveAsync(fieldContext);
await Verify(result);
}
[Fact]
public Task RunInvalidInputQuery()
{
Thread.CurrentThread.CurrentUICulture = new("en-US");
var field = new Query().GetField("inputQuery")!;
var userContext = new GraphQLUserContext();
FluentValidationExtensions.AddCacheToContext(
userContext,
ValidatorCacheBuilder.Instance);
var value = new Dictionary<string, object>();
var fieldContext = new ResolveFieldContext
{
Arguments = new Dictionary<string, ArgumentValue>
{
{
"input", new ArgumentValue(value, ArgumentSource.Variable)
}
},
UserContext = userContext
};
var exception = Assert.Throws<ValidationException>(
() => field.Resolver!.ResolveAsync(fieldContext));
return Verify(exception.Message);
}
}
snippet source | anchor
Icon
Shield designed by Maxim Kulikov from The Noun Project