Is there a clear way to plugin 3rd party validation library like FluentValidation? If there is then documentation needs to be updated. If not then can it be added as feature request so that its easy to use 3rd party plugin?
Fluent Validation for e.g. works really well with asp.net webapi where its completely unintrusive about how you declare your validation rules and do remote validation etc. Its widely used and populer so it would be easier for those who wishes to migrate from webapi to hotchocolate.
There is since you can completely customs the execution pipeline. The question is where to put such an integration. So, we could validate all arguments of the syntaxttree on validation, which would be fast but variables are not yet coerced. Also the arguments are only available as literals.
Or, it could be added as a query middleware after the variable coercion.
Lastly you could write a field middleware to handle this and this Is probably the simplest way to do it.
The middleware is able to run before or after the resolver. The middleware has generic access to all arguments and could integrate nicely with fluent validation.
If you want to contribute something like that we are open for that.
In any case we can walk you through implementing this.
As a side note @nigel-sampson investigated doing something like that.
I am doing following
descriptor.Field<SalesPersonMutations>(f => f.CreateSalesPerson(default))
.Use((services, next) => new MyValidationMiddleware<SP_GetSalesPersons>(next, new CreateSalesPersonValidator()));
The middleware is as follows:
internal class MyValidationMiddleware<T>
{
private readonly FieldDelegate _next;
private readonly AbstractValidator<T> _validator;
public MyValidationMiddleware(FieldDelegate next, AbstractValidator<T> validator)
{
_next = next;
_validator = validator;
}
public async Task InvokeAsync(IMiddlewareContext context)
{
var test = context.ContextData.TryGetValue(nameof(T), out var o);
// test returns as false.
// TODO this._validator.Validate<T>(myinputArguments);
await _next(context);
}
}
The validation middleware is executing . However I am not sure how to get the myinputArguments from the context. Any idea?
Just as a tip, you can also use classes to represent a middleware.
.Use<MyValidationMiddleware<SP_GetSalesPersons>>();
That should work, we will handle all the constructor injection for you.
Regarding the arguments.
If you want a specific argument you can do the following:
var abc = context.Argument<string>("argumentName");
do not want to specify the type but access the argument in a more generic way do the following:
var abc = context.Argument<object>("argumentName");
if you want to fetch the argument as a literal:
var abc = context.Argument<IValueNode>("argumentName");
If you want to iterate over all arguments that the field has you can do the following:
foreach(var argument in context.Field.Arguments)
{
...
}
If you want to see which arguments where actually provided then you have to look at the field selection
foreach(var argument in context.FieldSelection.Arguments)
{
}
When you access the selection than you basically access the parsed syntax tree whereas if you access Field then you are accessing the type information ...you can compare that to MethodInfo in .NET.
Does that help you?
It does help.
var abc = context.Argument<object>("argumentName");
This is great stuff.
Let me know anyway I can contribute to this.
The next challenge for me is to how throw error to the client. I have seen the ErrorBuilder.New() and QueryValidationResult but no idea how to use it properly so that it put these validation result in corrrect place.
This is what I done with my validation middleware
public class MyValidationMiddleware<T>
{
private readonly FieldDelegate _next;
private readonly AbstractValidator<T> _validator;
public MyValidationMiddleware(FieldDelegate next, AbstractValidator<T> validator)
{
_next = next;
_validator = validator;
}
public async Task InvokeAsync(IMiddlewareContext context)
{
var input = context.Argument<T>("input");
if (input != null)
{
ValidationResult result = await this._validator.ValidateAsync<T>(input);
if (!result.IsValid)
{
SetResult(context, result);
return;
}
await _next(context).ConfigureAwait(false);
return;
}
await _next(context).ConfigureAwait(false);
return;
}
private void SetResult(IMiddlewareContext context, ValidationResult result)
{
List<IError> errors = new List<IError>();
foreach (var validationError in result.Errors)
{
errors.Add(ErrorBuilder.New()
.SetMessage(validationError.ErrorCode)
.SetExtension(validationError.PropertyName, validationError.ErrorMessage)
.SetPath(context.Path)
.Build());
}
context.Result = errors;
}
}
which generates following
{
"errors": [
{
"message": "NotEmptyValidator",
"path": [
"createSalesPerson"
],
"extensions": {
"PCFullName": "FullName required"
}
},
{
"message": "NotEmptyValidator",
"path": [
"createSalesPerson"
],
"extensions": {
"GenderId": "Gender required"
}
}
],
"data": {
"createSalesPerson": null
},
}
Does this looks ok?
yes, this exactly the right way.
Thank you @activebiz, this is exactly what we needed. I added some syntactic sugar with this UseValidator extension method:
public static class ObjectFieldDescriptorExtensions
{
public static IObjectFieldDescriptor UseValidator<TValidator, TInput>(this IObjectFieldDescriptor descriptor)
where TValidator : AbstractValidator<TInput>, new()
{
return descriptor.Use(
(services, next) =>
new MyValidationMiddleware<TInput>(
next,
new TValidator()));
}
}
We plug it in like this:
descriptor.Field(q => q.FileUploadProcessAsync(default, default))
.Name("fileUpload")
.Description("Upload a file to Azure.")
.Type<FileUploadAzureFunctionResultType>()
.Argument(
"input",
a => a.Type<NonNullType<FileUploadAzureFunctionInputType>>()
.Description("file upload parameters."))
.UseValidator<FileUploadAzureFunctionInputValidator, FileUploadAzureFunctionInput>()
.Authorize();
Hope that someday HC has some kind of built-in hook for FluentValidation or the like (or in a separate Hotchocolate.Validation library). But your extensible middleware makes this sort of thing possible. Loving the HC.
@PrimeHydra this is on our list. We also plan to support the validation attributes that are located in the ComponentModel namespace.
@PrimeHydra this is on our list. We also plan to support the validation attributes that are located in the ComponentModel namespace.
any update on this?
Hope that someday HC has some kind of built-in hook for FluentValidation or the like (or in a separate Hotchocolate.Validation library)
@PrimeHydra
I've started something that does exactly this:
https://github.com/benmccallum/fairybread
FYI, it's actually more generic than the implementation above, where you're explicitly needing to call UseValidator to setup the middleware and define <T>. I took a different approach and just find any validator for the type of the argument and running it/them.
Should v1 in the next couple of weeks, just trying to find time to wrap up the last bits.
Most helpful comment
@PrimeHydra this is on our list. We also plan to support the validation attributes that are located in the ComponentModel namespace.