How to discern between model binding errors and model validation errors?
Asked Answered
S

2

7

I'm implementing a REST API project using ASP.NET Core MVC 2.0, and I'd like to return a 400 status code if model binding failed (because the request is syntactically wrong) and a 422 status code if model validation failed (because the request is syntactically correct but contains unacceptable values).

As an example, given an action like

[HttpPut("{id}")]
public async Task<IActionResult> UpdateAsync(
    [FromRoute] int id,
    [FromBody] ThingModel model)

I'd like to return a 400 status code when the id parameter in the route contains a non-digit character or when no body has been specified in the request and a 422 status code when the properties of ThingModel contain invalid values.

From what I've seen both IValueProvider and IModelBinder implementations add their errors to the request's ModelStateDictionary like the validators do, and there is no way to inject code between binding and validation.

How can I implement such a behavior?

Spada answered 29/8, 2017 at 14:6 Comment(0)
H
3

Haven't checked option #1 yet, but:

  • ModelState.ValidationState has 4 possible values (Unvalidated, Invalid, Valid, Skipped) where in case of model binding errors I do get Unvalidated as a value
  • Also would consider using ApiBehaviorOptions (see sample here) to automatically return a new BadRequestObjectResult(actionContext.ModelState) - since in case of binding error with a NULL bound value there's nothing to do and in case of validation errors we probably can't do anything either.

Quick notes for the ApiBehaviorOptions:

  • must use ApiController attribute (which requires also routing attribute on the controller level also and does alter the way binding works)
  • the default behaviour with ApiController attribute will return a BadRequestObjectResult(actionContext.ModelState) without any extra code and configuration
  • if you decide to roll your own ApiBehaviorOptions - you must initialize it after service.AddMvc or need to use: services.PostConfigure(o => {}) which has similar effect
  • ApiBehaviorOptions SuppressModelStateInvalidFilter has to be false for the InvalidModelStateResponseFactory to work

So in some cases a custom filter is a better solution (less changes).

Hypothermal answered 16/10, 2018 at 7:23 Comment(4)
Relates to: HttpPost FromBody properties does not bind (are null) - see: #24130419Hypothermal
Thanks for "you must initialize it after service.AddMvc".Verile
First bulletpoint is not gonna be any good. Following cases all return value Invalid: a) no body, b) failed model binding (due to invalid data type) and c) failed model validation (syntax is ok, but data does not pass validation criteria).Hasheem
You could go for ModelState.Root.ValidationState, but that still would not be precise enough as you would get values Invalid/Unvalidated/Unvalidated, so you could not discern cases b) and c) from comment above.Hasheem
B
0

Add attribute below in your project

public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        base.OnActionExecuting(context);

        if (!context.ModelState.IsValid)
            context.Result = new BadRequestObjectResult(new
            {
                message = context.ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage))
            });
    }
}

Change your api like this

[HttpPut("{id}")]
[ValidateModel]
public async Task<IActionResult> UpdateAsync(
    [FromRoute] int id,
    [FromBody] ThingModel model)

id contains a non-digit => 400

model does not pass annotation validation inside => 400

if you want to reject unacceptable values in model with 422 code, implement in your controller

Baal answered 1/9, 2017 at 4:26 Comment(1)
Unless I did something wrong, when a filter/attribute is executed the model validation has already occurred, you already have in the ModelState both the binding errors and the validation errors.Spada

© 2022 - 2024 — McMap. All rights reserved.