2

I am having trouble understanding the model binding process in Asp.Net core 2. I have a very simple API that has a model. It has some basic validation on it. Whenever a user posts an incorrect model, I am trying to return a 422 unprocessableentity along with the error messages from the modelstate.

The 2 issues I am trying to understand are as follows:

  • If I post a request without an ID, a default ID of 0 is being created circumventing the required attribute. I am assuming this is C# functionality for providing default values to fields. Is there a way to circumvent this?

  • The other problem is that if I place a breakpoint in my post action and send a bad request, it does not even go into the method. It sends back a 400 bad request by using the validation attributes. How does this work? Does the request halt as soon as it tries to model bind to an invalid property (i.e. Name length > 10)? What I need it to do is send back a 422 unprocessable entity with the same error message instead of 400.

Does ASP.NET not even go into the method if the model state validation fails based on the validation attributes? What would be a better way to solve this issue to return a 422 error code?

Below is the code for my various classes (I used the API template when creating the project):

Startup.cs - Only thing I added here was the singleton instance of my in-memory context

public void ConfigureServices(IServiceCollection services)
{
    //services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddMvc();
    services.AddSingleton<IItemRepository, ItemRepository>();
}

IItemRepository.cs - My interface for DI

public interface IItemRepository
{
    List<ItemModel> Items { get; set; }
    void AddValue(ItemModel itemModel);
}

ItemRepository.cs - Concrete implementation

public class ItemRepository : IItemRepository
{
    public List<ItemModel> Items { get; set; } = new List<ItemModel>();

    public ItemRepository()
    {
        Items.AddRange(
            new List<ItemModel> {
                new ItemModel {Id = 1, Name = "Test1" },
                new ItemModel {Id = 2, Name = "Test2" }
             }
        );
    }

    public void AddValue(ItemModel itemModel)
    {
        Items.Add(itemModel);
    }
}

ItemModel.cs - My model class for user input

public class ItemModel
{
    [Required]
    public int Id { get; set; }
    [MaxLength(10)]
    public string Name { get; set; }
}

ValuesController.cs

[Route("api/[controller]")]
[ApiController]
public class ValuesController : Controller
{
    private IItemRepository _context;

    public ValuesController(IItemRepository context)
    {
        _context = context;
    }

    // GET api/values
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return Ok(_context.Items);
    }

    // GET api/values/5
    [HttpGet("{id}", Name = "GetSingle")]
    public ActionResult<string> Get(int id)
    {
        return Ok(_context.Items.Where(x => x.Id == id));
    }

    // Problem here - placing a breakpoint in below method does not do anytthing as it will return a 400 bad request instead of 422
    [HttpPost]
    public ActionResult Post([FromBody] ItemModel itemModel)
    {
        if (!ModelState.IsValid)
        {
            return new UnprocessableEntityObjectResult(ModelState);
        }

        ItemModel addNew = new ItemModel { Id = itemModel.Id, Name = itemModel.Name };
        _context.AddValue(addNew);
        return Ok(addNew);
    }
}
Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
Help123
  • 1,511
  • 2
  • 28
  • 46
  • `send a bad request` Could you show us a sample of bad request? – Win Aug 27 '18 at 23:02
  • Make your `Id` property (of a view model) nullable and `[Required]` - [What does it mean for a property to be Required and nullable?](https://stackoverflow.com/questions/43688968/what-does-it-mean-for-a-property-to-be-required-and-nullable/43689575#43689575) –  Aug 27 '18 at 23:06

3 Answers3

4

For your first issue, if you don't want to make the property nullable, you can also put a range attribute [Range(1, int.MaxValue)], but 0 will not be a valid value in this case.

For your second issue, if you still want the automatic model validation from ApiControllerAttribute but want a 422 response code instead of 400 you can use the InvalidModelStateResponseFactory configuration option.

services.Configure<ApiBehaviorOptions>(options => 
{
  options.InvalidModelStateResponseFactory = ctx => 
     new UnprocessableEntityObjectResult(ctx.ModelState);
});
2

If I post a request without an ID, a default ID of 0 is being created circumventing the required attribute. I am assuming this is C# functionality for providing default values to fields. Is there a way to circumvent this?

As @StephenMuecke answered here, you need to change your model to

public class ItemModel
{
    [Required]
    public int? Id { get; set; }

    [MaxLength(10)]
    public string Name { get; set; }
}

The other problem is that if I place a breakpoint in my post action and send a bad request, it does not even go into the method. It sends back a 400 bad request by using the validation attributes. How does this work? Does the request halt as soon as it tries to model bind to an invalid property (i.e. Name length > 10)? What I need it to do is send back a 422 unprocessable entity with the same error message instead of 400.

This is because you applied the ApiControllerAttribute to the Controller. From the documentation:

Validation errors automatically trigger an HTTP 400 response. The following code becomes unnecessary in your actions:

if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}

You can either remove the attribute, or, as the same link explains, add this to the startup configuration:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = true;
})
Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
  • 1
    Thank you and @StephenMuecke as well. This answers my question. I didn't realize with the new apicontroller attribute, it automatically triggers a bad request. Using the nullable type on my ID fixed the issue. – Help123 Aug 27 '18 at 23:53
1

Your first issue can be solved by making the property nullable. As commented by Stepen Muecke.

Also take a look here, perhaps the BindRequired attribute can help. The article also describes how to tweak behaviour.

For your second issue, this is new (breaking) behaviour by Asp.Net Core 2.1. New is the automatic 400 response. That explains why your breakpoint isn't hit. You can suppress this as follows:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = true;
});