Use custom validation responses with fluent validation
Asked Answered
D

7

29

Hello I am trying to get custom validation response for my webApi using .NET Core.

Here I want to have response model like

[{
  ErrorCode:
  ErrorField:
  ErrorMsg:
}]

I have a validator class and currently we just check ModalState.IsValid for validation Error and pass on the modelstate object as BadRequest.

But new requirement wants us to have ErrorCodes for each validation failure.

My sample Validator Class

public class TestModelValidator :  AbstractValidator<TestModel>{

public TestModelValidator {
   RuleFor(x=> x.Name).NotEmpty().WithErrorCode("1001");
   RuleFor(x=> x.Age).NotEmpty().WithErrorCode("1002");
  }
}

I can use something similar in my actions to get validation result

Opt1:

 var validator = new TestModelValidator();
    var result = validator.Validate(inputObj);
    var errorList = result.Error;

and manipulate ValidationResult to my customn Response object. or
Opt2:

I can use [CustomizeValidator] attribute and maybe an Interceptors.

but for Opt2 I don't know how to retrieve ValidationResult from interceptor to controller action.

All I want is to write a common method so that I avoid calling Opt1 in every controller action method for validation.

Request to point me to correct resource.

Dextroamphetamine answered 18/8, 2017 at 13:35 Comment(0)
D
3

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.

Dextroamphetamine answered 30/8, 2017 at 8:52 Comment(1)
just ran into a similar problem and ended up with the unfortunate side-effect of having to catch errors when manually throwing validate... I ended up implementing a validator interceptor and checking/throwing the exception in the AfterAspNetValidation methodBartel
B
38

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

Binoculars answered 11/12, 2018 at 11:44 Comment(3)
I don't see where you are getting the field and the error code here? Were you able to do that somehow?Cruet
Hello bbqchickenrobot. The errors are retrieved from context.ModelState. Its are inserted automatic by the fluentvalidation rules when checking the model. a simple example: public class UserGetValidation : AbstractValidator<UserGetRequest> { public UserGetValidation() { RuleFor(m => m.UserId).NotEmpty().GreaterThan(0); } }Binoculars
The requirement is to respond with the FluentValidation ErrorCode for each validation failure. This solution only appears to respond with the error ErrorMessage.Toting
P
26

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
      });
Phosphor answered 25/1, 2019 at 13:25 Comment(2)
This answer worked best for me as well! Another consideration to make is returning Microsofts built-in ValidationProblemDetails class. This is used under the covers of FluentValidation. More details about it can be found here too.Elver
This doesn't address the new requirement to have ErrorCodes for each validation failure. The requirement is to use the FluentValidation error codes from the ValidationResult.Toting
T
14

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>()
        });
    }
}
Toting answered 8/4, 2020 at 16:12 Comment(3)
Minor fix in your ValidationErrorModel constructor, there are missing semi-colons for each property assignmentUrian
Tried this in Core 3.1 and it does not even hit the breakpoint when I send an invalid request. Seems like routing does short before the filters even get called when validation fails... Tried everything, but can't get it to run. Only way is implementing IResultFilter but then you cannot manipulate the response anymore.Schwab
For everyone having the same issue: services.ConfigureApiBehaviorOptions(o => o.SuppressModelStateInvalidFilter = true) is the solution to it.Schwab
D
3

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.

Dextroamphetamine answered 30/8, 2017 at 8:52 Comment(1)
just ran into a similar problem and ended up with the unfortunate side-effect of having to catch errors when manually throwing validate... I ended up implementing a validator interceptor and checking/throwing the exception in the AfterAspNetValidation methodBartel
T
2

Similar to Alexander's answer above, I created an anonymous object using the original factory I could find in the source code, but just changed out the parts to give back a custom HTTP response code (422 in my case).

ApiBehaviorOptionsSetup (Original factory)

services.AddMvcCore()
...
// other builder methods here
...
.ConfigureApiBehaviorOptions(options =>
                {
                    // Replace the built-in ASP.NET InvalidModelStateResponse to use our custom response code
                    options.InvalidModelStateResponseFactory = context =>
                    {
                        var problemDetailsFactory = context.HttpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
                        var problemDetails = problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState, statusCode: 422);
                        var result = new UnprocessableEntityObjectResult(problemDetails);
                        result.ContentTypes.Add("application/problem+json");
                        result.ContentTypes.Add("application/problem+xml");
                        return result;
                    };
                });

Transmontane answered 17/3, 2021 at 18:40 Comment(0)
S
1

Here I tried

 public async Task OnActionExecutionAsync(ActionExecutingContext context,
                                       ActionExecutionDelegate next)
        {
            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 value = context.ModelState.Keys.ToList();
                Dictionary<string, string[]> dictionary = new Dictionary<string, string[]>();
                foreach (var modelStateKey in context.ModelState.Keys.ToList())
                {
                    string[] arr = null ;
                    List<string> list = new List<string>();
                    foreach (var error in context.ModelState[modelStateKey].Errors)
                    {
                        list.Add(error.ErrorMessage);
                    }
                    arr = list.ToArray();
                    dictionary.Add(modelStateKey, arr);
                }
                var responseObj = new
                {
                    StatusCode="400",
                    Message = "Bad Request",
                    Errors = dictionary
                };
 

        context.Result = new BadRequestObjectResult(responseObj);
                return;
            }
            await next(); 
        }

Response Model:
{
    "statusCode": "400",
    "message": "Bad Request",
    "errors": {
        "Channel": [
            "'Channel' must not be empty."
        ],
        "TransactionId": [
            "'TransactionId' must not be empty."
        ],
        "Number": [
            "'Number' must not be empty."
        ]
    }
}

Swell answered 13/6, 2022 at 5:39 Comment(2)
You tried this but did it work? If so, be aware that code in an answer is best accompanied by some explanation of what it's doing or how it works.Spiny
Yes. It works ..Ok I will add explanation ..Swell
P
0

Here is a generic solution that I used to get the Fluent Validation ErrorCode and perform custom logic based on it using an ActionFilter:

In Program.cs we hook up the filter to all api methods:

builder.Services.AddControllers(options =>
{
    options.Filters.Add<CustomValidationFilter>();
});

And we create it like this:

public class CustomValidationFilter : IActionFilter
{
    
    public void OnActionExecuting(ActionExecutingContext context)
    {
        foreach (var argument in context.ActionArguments.Values)
        {
            if (argument is IClientApiModel) 
            {
                // here we instantiate the validator using reflection
                // and we execute the validation manually

                var validatorType = typeof(IValidator<>).MakeGenericType(argument.GetType());
                var validator = (IValidator)context.HttpContext.RequestServices.GetService(validatorType);
                if (validator != null)
                {
                    var validationContextType = typeof(ValidationContext<>);
                    var contextType = validationContextType.MakeGenericType(argument.GetType());
                    var validationContext = (IValidationContext)Activator.CreateInstance(contextType, argument);
                    
                    var validationResult = validator.Validate(validationContext);
                    
                    HandleValidationResult(context, validationResult);
                }
            }
        }
    }

    private static void HandleValidationResult(ActionExecutingContext context, ValidationResult validationResult)
    {
        if (!validationResult.IsValid )
        {
            context.Result = new JsonResult(new CustomResponse(validationResult))
            {
                // this is not mandatory. It will be BadRequest by default
                StatusCode = GetStatusCode(validationResult)
            };
        }
    }

    private static int GetStatusCode(ValidationResult validationResult)
    {
         // here you can add custom logic to return different HttpStatusCodes 
         // depending on the errorCodes specific to your business needs
         // e.g. 
         // if(validationResult.Errors.Where(e => e.ErrorCode.Equals("ProductNotFound").Any())
         // return (int)HttpStatusCode.NoContent 
         // etc
    }

}

Here we have the CustomResponse class which realy can have any form. The idea is to look into the ValidationResult object, extract the errors, then arrange them to suite your needs.

public class CustomResponse
{
    public List<string> ErrorCodes { get; set; } 
    public List<string> ErrorMessages { get; set; }

    public CustomResponse(ValidationResult result)
    {
        ErrorCodes = new List<string>();
        ErrorMessages = new List<string>();

        foreach (var failure in result.Errors)
        {
            ErrorCodes.Add(failure.ErrorCode);
            ErrorMessages.Add(failure.ErrorMessage);
            
            //...perform special logic depending on the failure.ErrorCode if necessary
        }
    } 
    
    //...other properties that you want to display in the response
}
Pentad answered 9/7 at 10:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.