简体   繁体   中英

Use custom validation responses with fluent validation

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.

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

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.

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."
        ]
    }
}

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;
                    };
                });

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM