Search code examples
javascriptc#ajaxasp.net-corexmlhttprequest

What is different in my Ajax vs XMLHttpRequest Call that lets my Server understand Ajax but not XMLHttpRequest?


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: "[email protected]"
   }

    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.


Solution

  • 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:

    1. Form fields
    2. The request body (For controllers that have the [ApiController] attribute.)
    3. Route data
    4. Query string parameters
    5. Uploaded files

    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.

    enter image description here

    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.

    1. Remove the line explicitly setting the content type to application/json: xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");

    2. 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.

    3. 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;.

    enter image description here

    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.