How do I fix a 400 Bad Request error in .Net Core POST operation?
You have to send the anti forgery token with your request if you want to use the decorator [ValidateAntiForgeryToken]
. See this link for more information.
Also, even if your model is invalid, you return View()
. That means you get a http status 200 even if you send wrong data.
Set a breakpoint on if(ModelState.IsValid)
and check if you enter in it. If not, check the format of your payload.
Hope it helps.
EDIT regarding your payload and your model : You need to provide an Id
to you payload because of the [Required]
decorator in your TaskViewModel. Or you need to get rid of the [Required]
attribute on Id
. If you don't, if (ModelState.IsValid)
will always be false.
Chris Pratt is right, you need to send __RequestVerificationToken
.
If you comment out [ValidateAntiForgeryToken]
attribute, it seems that you send data from Body-raw-JSON, then you need to use [FromBody] to access data.
[HttpPost]
public async Task<IActionResult> Create([Bind("Assignee,Summary,Description")] [FromBody] TaskViewModel taskViewModel)
If you do not want to add [FromBody], you could send data using form-data
There's a number of issues here. First and foremost, why are you saving your view model to the database. This is actually an entity in this case, not a view model. You should definitely be using a view model, but you should also have a separate entity class. Then, your view model should only contain properties that you want to actually allow the user to edit, negating the need entirely for the Bind
attribute, which should be avoided anyways. (see: Bind is Evil).
// added "Entity" to the name to prevent conflicts with `System.Threading.Task`
[Table("Tasks")]
public class TaskEntity
{
[Key]
public long Id { get; set; }
[Required]
public string Summary { get; set; }
[Required]
public string Description { get; set; }
[Required]
public string Assignee { get; set; }
}
public class TaskViewModel
{
[Required(ErrorMessage = "Please provide Task Summary")]
[Display(Name = "Summary")]
public string Summary { get; set; }
[Required(ErrorMessage = "Please enter task description")]
[Display(Name = "Description")]
public string Description { get; set; }
[Required(ErrorMessage = "Please select Assignee")]
[Display(Name = "Assign To")]
public string Assignee { get; set; }
}
Also, note the division of responsibility. The entity has only things that matter to the database ([Required]
here indicates that the column should be non-nullable). Whereas the view model is concerned only with the view. There's no Id
property, since it's not needed or desired, and the display names and error messages to be presented to the user are placed here only.
Then, you'll need to map from your view model to your entity class:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(TaskViewModel model)
{
if (!ModelState.IsValid)
return View(model);
var task = new TaskEntity
{
Assignee = model.Assignee,
Summary = model.Summary,
Description = model.Description
};
_context.Add(task);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
The mapping here is fairly straight-forward, but you may prefer to utilize a library like AutoMapper to handle this for you: _mapper.Map<TaskEntity>(model)
.
While this is specifically for a create action, it's worth pointing out the subtle difference for an update. You'll want to first retrieve the existing task from your database and then map the posted values onto that. The rest remains relatively the same:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Update(long id, TaskViewModel model)
{
if (!ModelState.IsValid)
return View(model);
var task = await _context.Tasks.FindAsync(id);
if (task == null)
return NotFound();
task.Assignee = model.Assignee;
task.Summary = model.Summary;
task.Description = model.Description;
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
Finally, as to the main problem from your question, there's two issues. First, this action is designed for a traditional HTML form post (x-www-form-urlencoded
). As such, it doesn't make sense to send JSON to it, and sending JSON to it will not work. To test it in Postman, you should send the request as x-www-form-urlencoded
. If you do not, then your model will essentially always be invalid, because nothing will be bound to your model from the post body.
In order to receive JSON, your param would need to have the FromBody
attribute applied to it ([FromBody]TaskViewModel model
). However, if you do that, you can no longer receive traditional form posts, and in this context, that's what's going to be sent. If you were sending via AJAX (where you could conceivably use JSON), then you should also be returning JSON or maybe PartialView
, but not View
or a redirect.
Lastly, you need to include the request verification token, which should be another key in the post body name __RequestVerificationToken
. To get the value to send, you'll need to load the GET version of the view, first, and inspect the source. There will be a hidden input with the value.