ASP.NET Core and Ajax
Table of Contents
-
- 2.1. Basic Model Binding Code
- 2.2. HTML
- 2.3. Request Data
-
- 3.1. Code
- 3.2. Request Data
-
- 4.1. Code
- 4.2. Request Data
-
- 5.1. Request Data
-
- 6.1. Request Data
Introduction
When changing a program from a regular form submission to an asynchronous process in ASP.NET Core Razor Pages, it's quite common for something that used to work to stop working (is it just me?). This document aims to organize the relationship between ASP.NET Core's model binding and Ajax requests as a personal memo.
The key points are:
- Request data format and Content-Type header
- Server-side definitions (meaning of attributes like
[BindProperty]and[FromBody])
If the request and server-side definitions are not consistent, data cannot be received correctly. In many cases, it doesn't even result in a runtime error, making it difficult to notice inconsistencies. Let's start by looking at the basics of model binding.
Razor Pages Model Binding
Basic Model Binding Code
Form submission and Razor Pages model binding are handled with code like this:
public class HogeClass
{
public string Name { get; set; } = "Hoge";
public int Age { get; set; } = 18;
}
public class HogePageModel : PageModel
{
[BindProperty]
public HogeClass Hoge { get; set; } = new();
public void OnPost()
{
Console.WriteLine($"Name: {Hoge.Name}, Age: {Hoge.Age}");
}
}
@page
@model HogePageModel
<form method="post">
<input asp-for="@Model.Hoge.Name" />
<input asp-for="@Model.Hoge.Age" />
<button type="submit">Post</button>
</form>
The [BindProperty] attribute is used to automatically bind (map) HTTP request POST data (form input values) to the Hoge property.
When executed, a page like this will be displayed (Figure 1. Page display).
When launched in debug mode, enter values into the form and click the Post button. You can confirm that the OnPost method is called and Hoge.Name and Hoge.Age are set to the input values.
HTML
Let's check the content of the generated HTML.
<form method="post">
<input type="text" id="Hoge_Name" name="Hoge.Name" value="Hoge" />
<input type="number" id="Hoge_Age" name="Hoge.Age" value="18" />
<button type="submit">Post</button>
<input name="__RequestVerificationToken" type="hidden" value="XXXXXX・・・(省略)" />
</form>
ASP.NET Core automatically generates id, name, and value attributes.
id: Model property names are converted with
_as a delimiter (Hoge.Name → Hoge_Name).
→ Used in JavaScript orlabeltags (<label for="...">).name: Field name for model binding (Hoge.Name).
→ Maps values to theHoge.Nameproperty on the server side during POST.value: Current value of the model ("Hoge").
→ Initial value of the form when the page is displayed.
The automatically generated name attribute serves as the key to specify the server-side property name for binding.
__RequestVerificationToken
This is a CSRF protection token automatically generated by ASP.NET Core. It is not directly related to model binding, but without this token, the request will be judged as invalid, the server-side processing will not be executed, and a400 (Bad Request)response will be returned.
Request Data
Next, let's examine the content of the request data.
As an example, if "Fuga" is entered for Name and 25 for Age:
Content-type: application/x-www-form-urlencoded
Request Body: Hoge.Name=Fuga&Hoge.Age=25&__RequestVerificationToken=CfDJ8PYW-uS7dDZDn6LlNPE8uvosZhCcbGyYoNCYQFcq6heF05q5Q6DN3n6FllCtn9wclH7flJVoHqPZBIj-TC9Q0MunNm1mKLy-0E4p6GOU0zcX9pVTjEQgZvWWJzLJpcn60xHjBytC6p38h83g8WYvFN4
Content-type
The Content-type is a header that indicates the data format of the request body. The server interprets the request body data based on this specification.application/x-www-form-urlencoded
Despite its long name, this is known asURL-encoded formatorform data format, which is the standard encoding format for sending data from a browser. Data is represented as "key=value" pairs, and each pair is concatenated with an&symbol.Request Body
A URL-encoded string data is sent. If split by the&symbol:Hoge.Name=Fuga Hoge.Age=25 __RequestVerificationToken=CfDJ8PYW-uS7dDZDn6LlNPE8uvosZhCcbGyYoNCYQFcq6heF05q5Q6DN3n6FllCtn9wclH7flJVoHqPZBIj-TC9Q0MunNm1mKLy-0E4p6GOU0zcX9pVTjEQgZvWWJzLJpcn60xHjBytC6p38h83g8WYvFN4The key used is the
nameattribute from the HTML, and the condition for binding is thatHoge.Namematches the C# property nameHoge.Name.
fetch
Code
Let's change to an Ajax process using fetch.
There is no need to change the .cs file.
Modify the .cshtml file as follows:
@page
@model HogePageModel
<form method="post">
<input asp-for="@Model.Hoge.Name" />
<input asp-for="@Model.Hoge.Age" />
<button type="button" id="button">Post</button>
</form>
<script>
document.getElementById('button').addEventListener('click', () => {
const form = document.querySelector('form');
const formData = new FormData(form);
fetch('/HogePage', {
method: 'POST',
body: formData
});
});
</script>
Remember to change the type of the button tag to button. The default is submit, so if you don't change it to button, a normal form submission will occur.
Request Data
Let's check the content of the request data.
Similar to the first example, if "Fuga" is entered for Name and 25 for Age:
Content-type: multipart/form-data; boundary=----WebKitFormBoundary5BiMG5LpjEL9onVI
Request Body:
------WebKitFormBoundary5BiMG5LpjEL9onVI
Content-Disposition: form-data; name="Hoge.Name"
Fuga
------WebKitFormBoundary5BiMG5LpjEL9onVI
Content-Disposition: form-data; name="Hoge.Age"
25
------WebKitFormBoundary5BiMG5LpjEL9onVI
Content-Disposition: form-data; name="__Invariant"
Hoge.Age
------WebKitFormBoundary5BiMG5LpjEL9onVI
Content-Disposition: form-data; name="__RequestVerificationToken"
CfDJ8PYW-uS7dDZDn6LlNPE8uvrM_Ia3TWGCbZ7iqw9j5rLr_WI7a2SIQ-KDi9YDccFmw0UXgnkiuhKZTf_BW61bmTmHJgjGctDkT7leMPas7Z9__pMqsKlEgRyrziT5iN-yO2E1f3vATsGFPJ_UHw1cQew
------WebKitFormBoundary5BiMG5LpjEL9onVI--
- Content-type:
multipart/form-data
WhenFormDatais used,multipart/form-datais automatically set as the Content-type. 1
multipart/form-datais a Content-type for sending multiple types of data (such as files) from a form. Although file transmission is not necessary in this example, whenFormDatais used, the request body's data format becomesmultipart/form-data, so the Content-type must bemultipart/form-data.
Request Body
This ismultipart/form-dataformatted data. Data is separated by a boundary string (in this example, "----WebKitFormBoundary5BiMG5LpjEL9onVI").Why can it be received?
The server-side.csfile has not been changed at all, yet data can be received correctly. This is because the[BindProperty]attribute supports bothapplication/x-www-form-urlencodedandmultipart/form-data. You might think that any data can be received if[BindProperty]is attached, but that is not the case. Let's look at an example of sending JSON data next.
fetch (JSON)
Code
Change the format of the transmitted data to JSON.
public class HogeClass
{
public string Name { get; set; } = "Hoge";
public int Age { get; set; } = 18;
}
public class HogePageModel : PageModel
{
public HogeClass Hoge { get; set; } = new();
public void OnPost([FromBody] HogeClass hoge)
{
Console.WriteLine($"Name: {hoge.Name}, Age: {hoge.Age}");
}
}
- Stop using
[BindProperty]. - Add
[FromBody] HogeClass hogeto theOnPostmethod's arguments.
[BindProperty] does not support JSON data, so it cannot be used.
To receive JSON data, you must apply the [FromBody] attribute to a method argument.
This way, model binding will occur to the method argument hoge, not to a property.
@page
@model HogePageModel
<form method="post">
<input asp-for="@Model.Hoge.Name" />
<input asp-for="@Model.Hoge.Age" />
<button type="button" id="button">Post</button>
</form>
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery
@{
var token = antiforgery.GetAndStoreTokens(HttpContext).RequestToken;
}
<script>
document.getElementById('button').addEventListener('click', () => {
const name = document.querySelector('[name="Hoge.Name"]').value;
const age = parseInt(document.querySelector('[name="Hoge.Age"]').value, 10);
const data = {
Name: name,
Age: age
};
fetch('/HogePage', {
method: "POST",
headers: {
'RequestVerificationToken': `@token`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
});
</script>
The modifications to the .cshtml file are as follows:
Set the CSRF token in the request header.
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery @{ var token = antiforgery.GetAndStoreTokens(HttpContext).RequestToken; }Set the CSRF token in the
RequestVerificationTokenrequest header. In regular form submissions andmultipart/form-data, it was set in the request body, but for JSON data, it will not be interpreted as a token if set in the body, so it must be set in the header.Set
Content-Typetoapplication/json.
Since JSON data is being sent,application/jsonmust be set. If not specified, it will be sent as the defaulttext/plain. If sent astext/plain, the server will not be able to receive it (aNullReferenceExceptionwill occur).Use
JSON.stringifyto convert the JavaScript object into a JSON formatted string before setting it in the body.
The request body data must be a string. If it remains an object, the server will not be able to receive it (aNullReferenceExceptionwill occur).
Request Data
The request data will be as follows:
Content-type: application/json
requestverificationtoken: CfDJ8PYW-uS7dDZDn6LlNPE8uvosZhCcbGyYoNCYQFcq6heF05q5Q6DN3n6FllCtn9wclH7flJVoHqPZBIj-TC9Q0MunNm1mKLy-0E4p6GOU0zcX9pVTjEQgZvWWJzLJpcn60xHjBytC6p38h83g8WYvFN4
Request Body: {"Name":"Fuga","Age":25}
$.ajax
Next is when using jQuery's $.ajax().
The .cs file can use the code from the first example as is.
public class HogeClass
{
public string Name { get; set; } = "Hoge";
public int Age { get; set; } = 18;
}
public class HogePageModel : PageModel
{
[BindProperty]
public HogeClass Hoge { get; set; } = new();
public void OnPost()
{
Console.WriteLine($"Name: {Hoge.Name}, Age: {Hoge.Age}");
}
}
The .cshtml file will be as follows:
@page
@model HogePageModel
<form method="post">
<input asp-for="@Model.Hoge.Name" />
<input asp-for="@Model.Hoge.Age" />
<button type="button" id="button">Post</button>
</form>
@section Scripts {
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
$('#button').on('click', () => {
$.ajax({
type: 'POST',
url: '/HogePage',
data: $('form').serialize()
});
})
</script>
Using jQuery's serialize() converts all input values within the form elements into a application/x-www-form-urlencoded formatted string.
Request Data
Content-type: application/x-www-form-urlencoded
Request Body: Hoge.Name=Fuga&Hoge.Age=25&__Invariant=Hoge.Age&__RequestVerificationToken=CfDJ8PYW-uS7dDZDn6LlNPE8uvqnfpJLTuGH7NvfcuVFLJ5_3LMGcH7tqAUE1RwSyMoFfST7WJmloCyFwehhiYiH2TPxF_dnxSkbWDVXrXqhpJhg7f1QxZwyGSUWUpa24AMxeDtSjqxh-nATdZzHh_oyxzs
The default Content-type for $.ajax is application/x-www-form-urlencoded, so it does not need to be explicitly specified.
$.ajax (JSON)
What if we want to use jQuery's $.ajax() while sending data as JSON?
The .cs file will be exactly the same as in the fetch (JSON) example.
public class HogeClass
{
public string Name { get; set; } = "Hoge";
public int Age { get; set; } = 18;
}
public class HogePageModel : PageModel
{
public HogeClass Hoge { get; set; } = new();
public void OnPost([FromBody] HogeClass hoge)
{
Console.WriteLine($"Name: {hoge.Name}, Age: {hoge.Age}");
}
}
Stop using [BindProperty] and add [FromBody] HogeClass hoge to the OnPost method's arguments.
@page
@model HogePageModel
<form method="post">
<input asp-for="@Model.Hoge.Name" />
<input asp-for="@Model.Hoge.Age" />
<button type="button" id="button">Post</button>
</form>
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery
@{
var token = antiforgery.GetAndStoreTokens(HttpContext).RequestToken;
}
@section Scripts {
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
$('#button').on('click', () => {
const name = document.querySelector('[name="Hoge.Name"]').value;
const age = parseInt(document.querySelector('[name="Hoge.Age"]').value, 10);
const data = {
Name: name,
Age: age
};
$.ajax({
type: 'POST',
url: '/HogePage',
headers: {
'RequestVerificationToken': '@token'
},
contentType: 'application/json',
data: JSON.stringify(data)
});
})
</script>
}
- Setting
RequestVerificationToken - Setting
contentType: 'application/json' - Converting the object to a string using
JSON.stringify
The necessary modifications are almost identical to the fetch (JSON) case.
Request Data
Content-type: application/json
Requestverificationtoken: CfDJ8PYW-uS7dDZDn6LlNPE8uvpeLThm5_Z8LiRmjO6nQ5eUodlnqHbI-kjmnqhLB_NuagWlOn4PWSDy_JEAnV9gNVyrhAULTXJe2-JbRbcAWkZCz1blHS68eF-RVG67CAc9nBXEEIqbRGW6V9V48Md2Vvs
Request Body: {"Name":"Fuga","Age":25}
It can be seen that JSON data is being sent.
Summary
To summarize, the following applies:
| Method | .cs Data Reception Method | Content-Type | Sent Data Format |
|---|---|---|---|
| Form Submission (POST) | Bind to [BindProperty] property |
application/x-www-form-urlencoded | URL Encoded |
| fetch | Bind to [BindProperty] property |
multipart/form-data | Multipart |
| fetch (JSON) | Bind to [FromBody] argument |
application/json | JSON |
| $.ajax | Bind to [BindProperty] property |
application/x-www-form-urlencoded | URL Encoded |
| $.ajax (JSON) | Bind to [FromBody] argument |
application/json | JSON |
To reiterate, the important points are:
- Match the sent data format with the
Content-Typespecification. - Change the server-side implementation according to the sent data format and
Content-Type.
I hope this is helpful.
References
contentType (default: 'application/x-www-form-urlencoded; charset=UTF-8') Type: Boolean or String When sending data to the server, use this content type. Default is "application/x-www-form-urlencoded; charset=UTF-8",
Note that if the body is a string, Content-Type defaults to text/plain. Therefore, we use the headers option to send application/json instead.
