3

Asp.net core 3.1 WebApi.

I have a model with required properties.

1.If model is not valid, then the response contains data like :

{
    "errors": {
        "Name": [
            "Update model can't have all properties as null."
        ],
        "Color": [
            "Update model can't have all properties as null."
        ]
    },
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "|f032f1c9-4c36d1e62aa60ead."
}

And this looks good for me.

But if I add some custom validation to modelState.AddModelError("statusId", "Invalid order status id.") then it returns different structure:

[
    {
        "childNodes": null,
        "children": null,
        "key": "statusId",
        "subKey": {
            "buffer": "statusId",
            "offset": 0,
            "length": 8,
            "value": "statusId",
            "hasValue": true
        },
        "isContainerNode": false,
        "rawValue": "11202",
        "attemptedValue": "11202",
        "errors": [
            {
                "exception": null,
                "errorMessage": "Invalid order status id."
            }
        ],
        "validationState": 1
    }
]

Also looks like ModelState.IsValid is actual no more for controller, because the bad request is returned before it even enters the controller with invalid mode. Or is there some flag with global validation via ModelSate?

Why the structure is different? How to make it the same? How to force to get to ModelState.IsValid method inside of api controller as it worked in MVC?

Update:



    [Route("....")]
    [Authorize]
    [ApiController]
    public class StatusesController : ApiControllerBase
    {


        [HttpPut, Route("{statusId}")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status409Conflict)]
        [Produces("application/json")]
        public async Task<ObjectResult> UpdateStatusAsync(int statusId, [FromBody] StatusUpdateDto orderStatusUpdateDto)
        {

            int companyId = User.Identity.GetClaimValue<int>(ClaimTypes.CompanyId);

            const string errorNotFound  = "There is not order status with this id for such company";
            if (statusId <= 0)
            {
                Logger.LogError(errorNotFound);
                ModelState.AddErrorModel(nameof(statusId), "Invalid order status id")
                throw new NotFound(ModelState);
            }
            
            if (orderStatusUpdateDto == null)
            {
                const string error = "Invalid (null) order status can't be added";
                Logger.LogError(error);
                throw new ArgumentNullException(error);
            }

            if (ModelState.IsValid == false) // also this code is always true or returns 400 before this line
            {
                return BadRequest(ModelState); 
            }

           ....
            return result;
        }

}
Artem A
  • 2,154
  • 2
  • 23
  • 30
  • Where do you add your error to the model state? Inside the controller method? Do you decorate the controller with the ApiController attribute? – treze Mar 22 '21 at 12:29
  • Inside the controller method - yes decorate the controller with the ApiController - yes – Artem A Mar 22 '21 at 12:33
  • In addition to the treze's answer below, you can configure the factory used to produce the result of invalid model with `.ConfigureApiBehaviorOptions(o => o.InvalidModelStateResponseFactory = ...)`. – King King Mar 22 '21 at 12:55

1 Answers1

6

The ApiController attribute adds some specific opinionated behaviors to a controller. One of them is to return a 400 error if the model is not valid. This behavior can be disabled, but only on a global level.

services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
    });

I think you have the following options:

  • Disable this behavior and check ModelState.IsValid yourself. Use ValidationProblem method to produce the same response
  • Add this check to the validator of the model
  • Keep everything as it is. But use ValidationProblem inside the controller method to return the validation errors.

See https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-3.1#automatic-http-400-responses for information

treze
  • 3,159
  • 20
  • 21
  • Thanks. This answers some of my questions. What do you think about different structure in returned result? – Artem A Mar 22 '21 at 12:59
  • What do you return as ActionResult when you add your custom error? If you return ValidationProblem(), you should get the same response. https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.validationproblem?view=aspnetcore-3.1 – treze Mar 22 '21 at 13:00
  • I've fixed the example buf I return BadRequest or NotFound (if id is less then 0) and pass ModelState to result in both cases – Artem A Mar 22 '21 at 13:04
  • Oh...looks like that the reason! Once I return NotFound - I get different result structure, then I return BadRequest (expected structure). Maybe Validation Factory has a specific implementation for bad requests and default for others... – Artem A Mar 22 '21 at 13:06
  • yes, as stated in the docs I linked in the answer "To make automatic and custom responses consistent, call the ValidationProblem method instead of BadRequest. ValidationProblem returns a ValidationProblemDetails object as well as the automatic response." – treze Mar 22 '21 at 13:15