Use custom validation responses with fluent validation
As for me, it's better to use the following code in ASP.NET Core project
services.AddMvc().ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = c =>
{
var errors = string.Join('\n', c.ModelState.Values.Where(v => v.Errors.Count > 0)
.SelectMany(v => v.Errors)
.Select(v => v.ErrorMessage));
return new BadRequestObjectResult(new
{
ErrorCode = "Your validation error code",
Message = errors
});
};
});
Also take into account that instead of anonymous object you can use your concrete type. For example,
new BadRequestObjectResult(new ValidationErrorViewModel
{
ErrorCode = "Your validation error code",
Message = errors
});
In .net core you can use a combination of a IValidatorInterceptor to copy the ValidationResult
to HttpContext.Items
and then a ActionFilterAttribute
to check for the result and return the custom response if it is found.
// If invalid add the ValidationResult to the HttpContext Items.
public class ValidatorInterceptor : IValidatorInterceptor {
public ValidationResult AfterMvcValidation(ControllerContext controllerContext, ValidationContext validationContext, ValidationResult result) {
if(!result.IsValid) {
controllerContext.HttpContext.Items.Add("ValidationResult", result);
}
return result;
}
public ValidationContext BeforeMvcValidation(ControllerContext controllerContext, ValidationContext validationContext) {
return validationContext;
}
}
// Check the HttpContext Items for the ValidationResult and return.
// a custom 400 error if it is found
public class ValidationResultAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext ctx) {
if(!ctx.HttpContext.Items.TryGetValue("ValidationResult", out var value)) {
return;
}
if(!(value is ValidationResult vldResult)) {
return;
}
var model = vldResult.Errors.Select(err => new ValidationErrorModel(err)).ToArray();
ctx.Result = new BadRequestObjectResult(model);
}
}
// The custom error model now with 'ErrorCode'
public class ValidationErrorModel {
public string PropertyName { get; }
public string ErrorMessage { get; }
public object AttemptedValue { get; }
public string ErrorCode { get; }
public ValidationErrorModel(ValidationFailure error) {
PropertyName = error.PropertyName;
ErrorMessage = error.ErrorMessage;
AttemptedValue = error.AttemptedValue;
ErrorCode = error.ErrorCode;
}
}
Then in Startup.cs
you can register the ValidatorInterceptor
and ValidationResultAttribute
like so:
public class Startup {
public void ConfigureServices(IServiceCollection services) {
services.AddTransient<IValidatorInterceptor, ValidatorInterceptor>();
services.AddMvc(o => {
o.Filters.Add<ValidateModelAttribute>()
});
}
}
Refer this link for answer: https://github.com/JeremySkinner/FluentValidation/issues/548
Solution:
What I've done is that I created a basevalidator class which inherited both IValidatorInterceptor and AbstractValidator. In afterMvcvalidation method if validation is not successful, I map the error from validationResult to my custom response object and throw Custom exception which I catch in my exception handling middleware and return response.
On Serialization issue where controller gets null object:
modelstate.IsValid will be set to false when Json Deserialization fails during model binding and Error details will be stored in ModelState. [Which is what happened in my case]
Also due to this failure, Deserialization does not continue further and gets null object in controller method.
As of now, I have created a hack by setting serialization errorcontext.Handled = true manually and allowing my fluentvalidation to catch the invalid input.
https://www.newtonsoft.com/json/help/html/SerializationErrorHandling.htm [defined OnErrorAttribute in my request model].
I am searching for a better solution but for now this hack is doing the job.
try with this:
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
I validate the model with fluentvalidation, after build the BadResquest response in a ActionFilter class:
public class ValidateModelStateAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var errors = context.ModelState.Values.Where(v => v.Errors.Count > 0)
.SelectMany(v => v.Errors)
.Select(v => v.ErrorMessage)
.ToList();
var responseObj = new
{
Message = "Bad Request",
Errors = errors
};
context.Result = new JsonResult(responseObj)
{
StatusCode = 400
};
}
}
}
In StartUp.cs:
services.AddMvc(options =>
{
options.Filters.Add(typeof(ValidateModelStateAttribute));
})
.AddFluentValidation(fvc => fvc.RegisterValidatorsFromAssemblyContaining<Startup>());
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
And it works fine. I hope you find it useful