'How to avoid InvalidModelStateResponseFactory to interfere with an error response already generated in FluentValidation
I am working on a .NET Core 3.0 MVC project using FluentValidation, for validating input from the front end made in React. I can't rely on that the input is validated in the front end.
I am using InvalidModelStateResponseFactory to catch conversion error e.g. "zz" to integer, and using FluentValidation for validate if a number is valid e.g. < 100.
My problem is that, when FluentValidation is catching an error, the InvalidModelStateResponseFactory catches it afterward, even if there is no conversion errors. And the error message from FluentValidation is lost.
The FluentValidation can validate whether an int is to large. e.g.:
public class XCommand
{
public int myInt {get;set;}
}
And the validator:
public class XCommandValidator : AbstractValidator<XCommand>
{
RuleFor(x => x.myInt).InclusiveBetween(0, 99).WithMessage("only below 100");
}
If the validator fails {myInt : 100}, I can catch the Exception and return a nice error response.
If the front end send {myInt : "zz"} I get an error generated by MVC (without InvalidModelStateResponseFactory involved):
{"errors":{"myInt":["Could not convert string to integer: zz. Path 'myInt', line 1, position 1."]},"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"One or more validation errors occurred.","status":400,"traceId":"|ae6d5a28-4822b30b8c0ae039."}
I want to to control the format of the error response I send to the front end, when I have an conversion error. To catch conversion errors I set up an InvalidModelStateResponseFactory in the Startup.cs:
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = contex =>
{
// create a nice error response
};
});
My problem is that if I don't have an conversion error, but an invalid value e.g. {myInt : 100} and it fails in the FluentValidator. InvalidModelStateResponseFactory is activated and the error response generated in the InvalidModelStateResponseFactory is send to the front end. The message generated in FluentValidation is lost.
How can I avoid InvalidModelStateResponseFactory to interfere with an error response already generated in FluentValidation?
Solution 1:[1]
The solution was to create a custom validation filter and that catches all validation errors from both MVC "native" and from FluentValidator.
public class CustomValidationFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context) { }
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
SortedList<string, List<string>> err = new SortedList<string, List<string>>();
foreach (var modelStateKey in context.ModelState.Keys)
{
var modelStateVal = context.ModelState[modelStateKey];
if (modelStateVal.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{
err.Add(modelStateKey, modelStateVal.Errors.Select(e => e.ErrorMessage).ToList());
}
}
var responseObj = new
{
Errors = err
};
context.Result = new JsonResult(responseObj)
{
StatusCode = 400
};
}
}
}
In StartUp.ConfigureServices this must be added:
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
Solution 2:[2]
My project based on Web API so I tried to avoid ActionFilter usage.
The solution is simple - get error from ErrorContext and return a custom error message.
In Startup.cs
services.Configure<ApiBehaviorOptions>(options
=> options.InvalidModelStateResponseFactory =
(context) =>
{
var error = context!.ModelState.FirstOrDefault().Value!.Errors.FirstOrDefault()!.ErrorMessage;
var result = new BadRequestResponse(error);
return new JsonResult(result);
});
And my error response class:
public class BadRequestResponse
{
public string Error { get; set; }
public BadRequestResponse(string error)
{
Error = error;
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Anders Finn Jørgensen |
Solution 2 | taigadev |