I have a very simple server call like this:
[HttpPost]
[AllowAnonymous]
public JsonResult Test(TestRequestModel requestModel)
{
//do stuff
return Json(new { result.Success });
}
My TestRequestModel looks like this:
public class TestRequestModel
{
public string Email { get; set; } = string.Empty;
}
I am trying to do a POST request to the server. But for a complex list of reasons I need to be using XMLHttpRequest instead of $.ajax. To do this I am going to show you how I did it in ajax and then how I did it with XMLHttpRequest.
First here is how I call my server:
function MyTestFunction() {
let parameters = {
Email: "test@test.com"
}
CallServer(function () {
//Do stuff
}, function () {
//Do other stuff
}, "/Home/Test", parameters)
}
Ajax:
function CallServer(resolve, reject, path, parameters) {
$.ajax({
type: "POST",
url: path,
data: AddAntiForgeryToken(parameters),
success: function (response) {
//do stuff
},
error: function (xhr) {
//do stuff
},
complete: function () {
//do more stuff
}
});
}
XMLHttpRequest Way:
function CallServer(resolve, reject, path, parameters) {
let xhr = new XMLHttpRequest();
xhr.open("POST", path, true);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.setRequestHeader('RequestVerificationToken', GetMyToken());
xhr.onreadystatechange = function (e) {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200 || xhr.status === 201) {
//do stuff
}
else {
//do other stuff
}
}
};
xhr.send(JSON.stringify(parameters));
}
If I run the above code the ajax way, then it works without issues. If I try to do it the XMLHttpRequest way then my request model gets created but the Email field is not populated. Now I found that I can solve this by adding [FromBody] on the request model and it does work, I tested it and no problems.
Also, as I read online and hopefully understood correctly, but ajax uses XMLHttpRequest behind the hood right?
So why do I have to add [FromBody] on my controller for this to work? Is there a way I can modify my XMLHttpRequest so that it is not needed? The solution I am looking for how to not have to specify [FromBody] on a json post request to .net core server.
The results you're experiencing are the result of Model Binding in ASP.NET.
The bottom line is that the two post
samples in your question -- $.ajax()
and ...xhr.send()
-- are not the same requests.
Request Method | Content type | Model Binding Source |
---|---|---|
$.ajax() |
application/x-www-form-urlencoded; charset=UTF-8 |
Form fields |
...xhr.send() |
application/json;charset=UTF-8 in request body |
Default model binding is not looking for data in the request body |
Referencing Microsoft's documentation on the sources for model binding,
By default, model binding gets data in the form of key-value pairs from the following sources in an HTTP request:
It's not explicitly stated in your question, but based on your sample controller, your project is ASP.NET MVC. Item 2 in the list above does not apply.
public JsonResult Test(TestRequestModel requestModel)
{
//do stuff
return Json(new { result.Success });
}
Just after the default list in the model binding documentation, the following is stated:
If the default source is not correct, use one of the following attributes to specify the source:
[FromQuery]
- Gets values from the query string.[FromRoute]
- Gets values from route data.[FromForm]
- Gets values from posted form fields.[FromBody]
- Gets values from the request body.[FromHeader]
- Gets values from HTTP headers..ajax()
sample
The .ajax()
method in your sample code is posting a request of content type:
application/x-www-form-urlencoded; charset=UTF-8
You can see this in the header of the post
request in the Network tab of the DevTools in Chrome.
By default, model binding for your MVC controller parses the request model.
XMLHttpRequest
sample
Your XMLHttpRequest
sample is sending the model data (containing the email field) in the body of the request via the send()
method:
xhr.send(JSON.stringify(parameters));
Model binding for your MVC controller is not looking at data in the body by default. This is why you're seeing an empty email field.
When you explicitly add the [FromBody]
attribute, the model binder looks for data in the body of the request.
Solution 1: Changing $.ajax()
post and using [FromBody]
attribute
You can make your $.ajax()
code to be equivalent to your XMLHttpRequest
code or vice versa. The following updates your $.ajax()
code to be consistent with the XMLHttpRequest
. That is, it sends your parameter
object in the request body.
$.ajax({
type: "POST",
contentType: "application/json",
url: path,
headers: {
RequestVerificationToken: GetMyToken()
},
data: JSON.stringify(parameters),
success: function (response) {
//do stuff
},
error: function (xhr) {
//do stuff
},
complete: function () {
//do more stuff
}
});
You'd need to keep the [FromBody]
attribute in your controller method:
public JsonResult Test([FromBody] TestRequestModel requestModel)
Solution 2: Changing XMLHttpRequest
post
You can change your XMLHttpRequest
to be almost equivalent to the $.ajax()
post by doing the following. (The explanation below shows the content type will be multipart/form-data;
instead of application/x-www-form-urlencoded
in the .ajax()
method in your original post. Default model binding sees the data as form fields in both cases, however.)
As you pointed out in your comment, you're now using xhr.send(parameters);
. However, you'd need to make other changes to get the MVC controller to use the default model binding. Here's an explanation of the XMLHttpRequest
code listed in @Rena's answer.
Remove the line explicitly setting the content type to application/json
:
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
With the removal of the setRequestHeader("Content-Type", ...)
method, the content type sent to the server is no set explicitly to application/json
. Therefore, stringifying the parameters
object (using xhr.send(JSON.stringify(parameters));
) is no longer necessary as the data being sent to the server will not be of type JSON.
Lastly, change the data type of the parameters
variable from a generic object
to the type FormData
.
With the above changes, the implementation of XMLHttpRequest
sets the content type header in the post
request based on the data type of the parameters
variable to multipart/form-data;
.
Going back to the default model binding list near the beginning of this answer (from under the header 'Sources' in Microsoft's model binding doc), the MVC controller now sees content type of the data in the request as item "1. Form fields" in the list.