Add FluentValidation support to GraphQL.net
See Milestones for release notes.
https://nuget.org/packages/GraphQL.FluentValidation/
Given the following input:
public class MyInput
{
public string Content { get; set; } = null!;
}
And graph:
public class MyInputGraph :
InputObjectGraphType
{
public MyInputGraph() =>
Field<StringGraphType>("content");
}
A custom validator can be defined as follows:
public class MyInputValidator :
AbstractValidator<MyInput>
{
public MyInputValidator() =>
RuleFor(_ => _.Content)
.NotEmpty();
}
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();
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
.
var options = new ExecutionOptions
{
Schema = schema,
Query = queryString,
Variables = inputs
};
options.UseFluentValidation(validatorCache);
var executionResult = await executer.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:
public class MyUserContext(string myProperty) :
Dictionary<string, object?>
{
public string MyProperty { get; } = myProperty;
}
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);
var options = new ExecutionOptions
{
Schema = schema,
Query = queryString,
Variables = inputs,
UserContext = new Dictionary<string, object?>
{
{
"MyUserContext",
new MyUserContext
(
myProperty: "the value"
)
}
}
};
options.UseFluentValidation(validatorCache);
If no instance is passed to ExecutionOptions.UserContext
:
var options = new ExecutionOptions
{
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
:
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
};
}
);
}
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:
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);
}
}
Unit tests can be run a specific field of a query:
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(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 input = new MyInput
{
Content = null!
};
var fieldContext = new ResolveFieldContext
{
Arguments = new Dictionary<string, ArgumentValue>
{
{
"input", new(input, ArgumentSource.Variable)
}
},
UserContext = userContext
};
var exception = Assert.Throws<ValidationException>(
() => field.Resolver!.ResolveAsync(fieldContext));
return Verify(exception.Message);
}
}
Shield designed by Maxim Kulikov from The Noun Project