Issue with Model Binding on POST in API - c#

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

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

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

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

Related

Null Models with default arguments need to be instantiated as such

I have the following asp.net WebApi2 route using .NET 4.6 that illustrates the problem I am having:
[Route("books/{id}")]
[HttpGet]
public JsonResponse GetBooks(string id, [FromUri]DescriptorModel model)
With the following model:
public class DescriptorModel
{
public bool Fiction { get; set; } = false;
// other properties with default arguments here
}
I am trying to allow Fiction property to be set to a default value (if not specified during the get request).
When I specify the Fiction property explicitly it works correctly:
curl -X GET --header 'Accept: application/json' 'http://127.0.0.1:11000/api/v1/books/516.375/?Fiction=false'
However, when doing the following test (omitting the property with the default argument):
curl -X GET --header 'Accept: application/json' 'http://127.0.0.1:11000/api/v1/books/516.375'
The value of "model" is bound as null which is not what I am looking for. My question is how to simply allow models defined with default values to be instantiated as such during/after the model binding process but prior to the controller's "GetBooks" action method being called.
NOTE. the reason I use models with GET requests is that documenting in swagger is much easier as then my GET/POST actions can reuse the same models in many case via inheritance.
Since you are using id as FromUri, the only way you can use a model with get is to use url with a query string
[Route("~/GetBooks/{id?}")]
[HttpGet]
public IActionResult GetBooks(string id, [FromQuery] DescriptorModel model)
in this case you url should be
'http://127.0.0.1:11000/api/v1/books/?Name=name&&fiction=true'
//or if fiction==false just
'http://127.0.0.1:11000/api/v1/books/?Name=name'
//or if want to use id
'http://127.0.0.1:11000/api/v1/books/123/?Name=name&&fiction=true'
using model your way will be working only with [FromForm] or [FromBody].
To use it as MVC recomends try this
[Route("books/{id}/{param1}/{param2}/{fiction?}")]
[HttpGet]
public JsonResponse GetBooks(string id, string param1, string param2, bool fiction)
By the way, you don't need to make bool false as default since it is false by default any way
if you want to use ID and DescriptorModel from uri you can do this only if you add Id to DescriptorModel too
[Route("books/{id}/{param1}/{param2}/{fiction?}")]
[HttpGet]
public JsonResponse GetBooks(DescriptorModel model)
UPDATE
If your mvc doesnt support [FromQuery], you can use RequestQuery inside of action like this
var value= context.Request.Query["value"];
but is better to update to MVC 6.
I wasn't able to figure out how to do this via model-binding but I was able to use Action Filters to accomplish the same thing.
Here's the code I used (note it only supports one null model per action but this could easily be fixed if needed):
public class NullModelActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext context)
{
object value = null;
string modelName = string.Empty;
// are there any null models?
if (context.ActionArguments.ContainsValue(null))
{
// Yes => iterate over all arguments to find them.
foreach (var arg in context.ActionArguments)
{
// Is the argument null?
if (arg.Value == null)
{
// Search the parameter bindings to find the matching argument....
foreach (var parameter in context.ActionDescriptor.ActionBinding.ParameterBindings)
{
// Did we find a match?
if (parameter.Descriptor.ParameterName == arg.Key)
{
// Yes => Does the type have the 'Default' attribute?
var type = parameter.Descriptor.ParameterType;
if (type.GetCustomAttributes(typeof(DefaultAttribute), false).Length > 0)
{
// Yes => need to instantiate it
modelName = arg.Key;
var constructor = parameter.Descriptor.ParameterType.GetConstructor(new Type[0]);
value = constructor.Invoke(null);
// update the model state
context.ModelState.Add(arg.Key, new ModelState { Value = new ValueProviderResult(value, value.ToString(), CultureInfo.InvariantCulture) });
}
}
}
}
}
// update the action arguments
context.ActionArguments[modelName] = value;
}
}
}
I created a DefaultAttribute class like so:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DefaultAttribute : Attribute
{
}
I then added that attribute to my descriptor class:
[Default]
public class DescriptorModel
{
public bool Fiction { get; set; } = false;
// other properties with default arguments here
}
And finally registered the action filter in
public void Configure(IAppBuilder appBuilder)
{
var config = new HttpConfiguration();
// lots of configuration here omitted
config.Filters.Add(new NullModelActionFilter());
appBuilder.UseWebApi(config);
}
I definitely consider this a hack (I think I really should be doing this via model binding) but it accomplishes what I needed to do with the constraints that I was given of ASP.NET (not Core) / WebApi2 / .NET Framework so hopefully some else will benefit from this.

Can't bind json in POST request body to class argument in .net-core

I'm trying to make a post request from my Angular frontend to the .net Core 3.1 backend, but in the controller method the argument object only gets by default 0 and null values;
let response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(obj)
});
[ApiController]
[Route("[controller]")]
[ApiExplorerSettings(IgnoreApi = true)]
public class RequestController : ControllerBase
{
[HttpPost]
public async Task<IAResponseViewModel> PostAnswer([FromBody] IAResponseViewModel something)
{
var temp = something.ToString();
if (!ModelState.IsValid)
{
Console.WriteLine();
}
return something;
}
}
public class IAResponseViewModel
{
public string AdministrationId { get; }
public int RoundUsesItemId { get; }
public int ResponseOptionId { get; }
}
The JSON object I see being submitted
{AdministrationId: "12345678-00e8-4edb-898b-03ee7ff517bf", RoundUsesItemId: 527, ResponseOptionId: 41}
When inspecting the controller method the 3 values of the IAResponseViewModel are null or 0
When I change the argument to object I get something with a value of
ValueKind = Object : "{"AdministrationId":"12345678-00e8-4edb-898b-03ee7ff517bf","RoundUsesItemId":523,"ResponseOptionId":35}"
I've tried with and without the [FromBody] attribute, changing the casing of the properties of the controller method's argument and the frontend argument, copy pasting the viewmodel attributes on to the submitted object keys, wrapping the posted object in a 'something' object. the ModelState.IsValid attribute shows as true.
I've red other answers such as Asp.net core MVC post parameter always null &
https://github.com/dotnet/aspnetcore/issues/2202 and others but couldn't find an answer that helped.
Why isn't the model binding working and how do I populate the viewmodel class I'm using with the json data?
From a comment on my original question:
Could it be because the properties in IAResponseViewModel are 'get only'?
Indeed this was the problem. Giving the properties (default) set methods fixed the problem.

How to pass null in body to endpoint within asp.net core 3.1

I have the following action within an asp.net core 3.1 controller
[ApiController]
[Route("example")]
public class MyExampleController : ControllerBase
{
[HttpPost("{id}/value")]
public async Task<IActionResult> Post(string id, [FromBody] int? value)
=> Task.FromResult(Ok());
}
This works fine if I post a body value of int (for example: 1, 2, etc...)
However, I can't find a way to get a null value passed in.
If I pass in an empty body or null body I get a status code of 400 returned with a validation message of A non-empty request body is required. returned.
I've also tried to change the value parameter to be an optional argument with a default value of null:
public async Task<IActionResult> Post(string id, [FromBody] int? value = null)
How do I pass in null to this action?
Finally figured this out, big thanks to #Nkosi and #KirkLarkin helping fault find this.
Within the Startup.cs when configuring the controllers into the container we just need to alter the default mvc options to AllowEmptyInputInBodyModelBinding
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(x => x.AllowEmptyInputInBodyModelBinding = true);
}
This way we can pass in null into the body of the post and it works perfectly fine. It also still applies the normal model validation via the attributes without having to check the ModelState manually:
public async Task<IActionResult> Post(string id,
[FromBody][Range(1, int.MaxValue, ErrorMessage = "Please enter a value bigger than 1")]
int? value = null)
Reference Automatic HTTP 400 responses
The [ApiController] attribute makes model validation errors automatically trigger an HTTP 400 response
This would explain the returned response.
Remove the [ApiController] to allow the invalid request to still make it to the controller action and also if the additional features of having that attribute is not critical to the current controller.
It would however require that the desired featured be applied manually
[Route("example")]
public class MyExampleController : ControllerBase {
[HttpPost("{id}/value")]
public async Task<IActionResult> Post(string id, [FromBody] int? value) {
if (!ModelState.IsValid) {
//...
return BadRequest(ModelState);
}
//...
return Ok();
}
}
It seems it has something to do with the way a single value is conducted through JSON. It expects a value and the null just simply creates an empty request body. You should consider defining a class like this
public class MyInt{
public int Value { get; set; }
public bool IsNull { get; set; }
}
[ApiController]
[Route("example")]
public class MyExampleController : ControllerBase
{
[HttpPost("{id}/value")]
public IActionResult Post(string id, [FromBody]MyInt value)
{
if(value.IsNull){
}
else{
}
return Ok();
}
}
In other words when you POST you post not just use a default value. You could do that the other way like this
[HttpPost("{id}/value")]
public IActionResult Post(string id, [FromBody]int value)...
[HttpGet("{id}/value")]
public IActionResult Get(string id)...//use the default value here

Sending data from DataTables to Web API controller using POST method

I try to make server-side processing for DataTables using Web API. There are two actions in my Web API controller with same list of parameters:
public class CampaignController : ApiController
{
// GET request handler
public dtResponse Get(int draw, int start, int length)
{
// request handling
}
// POST request handler
public void Post(int draw, int start, int length)
{
// request handling
}
}
If I use GET method to send AJAX request to the server, the Get action is activated. However, if I use POST method, then neither action are activated.
I tried to change POST handler signature to
public void Post([FromBody]object value)
{
// request handling
}
In this case the value is null. Note, the HttpContext.Current.Request.Form collection isn't empty. The draw, start, length variables are exist in this collection. Thus, I think the trouble is in model binding, but I cannot fix it. Help me, please.
Not knowing exactly what's going on, but appears there are a few missing elements. I've written a Post endpoint this morning, So hopefully will help pushing you in the right direction.
Also to note if you want "Data" use Get, if your inserting data then POST
[HttpPost]
[Route("orders")]
public async Task<IHttpActionResult> Post([FromBody]List<Models.Model.Order> orders)
{
if (orders == null)
return BadRequest("Unusable resources.");
if (validatedOrders.Count <= 0)
return BadRequest("Unusable resources.");
try
{
//Create abstracted Identity model to pass around layers
var identity = User.Identity as ClaimsIdentity;
var identityModel = IdentityModel.Create(identity);
if (identityModel == null)
return StatusCode(HttpStatusCode.Forbidden);
var response = await _orderService.AddAsync(validatedOrders, identityModel);
return Ok(response);
}
catch (System.Exception ex)
{
return InternalServerError();
}
finally
{
_orderService.Dispose();
}
}
Utilizing IHttpActionResult will expose the returning of a response
Verb attributes helps with building/designing Restful API's and same
name signatures
Attribute Routing saves writing in the config and change routes in
the class
To wrap it all up, replace the order collection with:
public sealed class Diagram
{
public int Draw { get; set; }
public int Start { get; set; }
public int Length { get; set; }
}
Rewrite the validation, remove the Identity creation, remove the insert and remove/replace the attribute routing.
With HttpContext.Current.Request.Form Try building a Diagram object from that and passing it up.
OR Alternatively passing in a form collection
[HttpPost]
[Route("something")]
// POST api/<controller>
public async Task<HttpResponseMessage> Post(FormDataCollection form)
{
string tid = form.Get("tid");
string sid = form.Get("sid");
string userid = form.Get("userid");
string udid = form.Get("udid");
}
Additional resource from the DataTable Docs
Git Hub Repo
What about this:
[HttpPost]
public void Post([FromBody]int draw, [FromBody]int start, [FromBody]int length)
{
// request handling
}

Handling a constructor that throws in a Controller method parameter?

I am making a small Asp.net Core Web API project. I have a model object that looks like this:
public class Order
{
public readonly int Quantity;
public Order(int quantity)
{
if (quantity < 0)
{
throw new ArgumentException("quantity");
}
this.Quantity = quantity;
}
}
and a controller that looks like this:
[Route("api/[controller]")]
public class OrderController : Controller
{
[HttpPost]
public IActionResult Post([FromBody]Order order)
{
// Do stuff...
return this.StatusCode(200);
}
}
If I pass an Order object with a positive quantity, the Post method works correctly. However, if I pass an Order with a negative quantity, the Order constructor throws an exception that is unhandled by my application or by asp.net.
What can I do to handle this exception in a graceful way? I notice that if I pass a request with invalid syntax (for instance, with badly formed JSON) there is no exception, and the order parameter has value 'null'. Can I set asp.net to do something similar? Or must I rewrite the Order constructor to not throw?
Use DataAnnotation to do what you are doing the MVC way.
public class Order
{
[Range(1, Int32.MaxValue)]
public int Quantity { get; set; }
}
Then in your controller do this:
[HttpPost]
public IActionResult Post([FromBody]Order order)
{
if (!ModelState.IsValid)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
// Do stuff...
return new HttpResponseMessage(HttpStatusCode.OK);
}
The other plus side is if you ever decide to use the Order model in an MVC web application (with views), you will get client side validation because of the Range attribute.

Categories

Resources