ASP.NET Core と Ajax
目次
-
- 2.1. 基本的なモデルバインディングのコード
- 2.2. HTML
- 2.3. リクエストデータ
-
- 5.1. リクエストデータ
-
- 6.1. リクエストデータ
はじめに
ASP.NET Core Razor Pages で通常のフォーム送信から非同期処理にプログラムを変更すると、変更前に動いていたものが動かなくなる、というのは割とよくありがちな事です(私だけ?)。ASP.NET Core のモデルバインディング と Ajax リクエストの関係について、備忘録もかねて整理します。
ポイントになるのは以下。
- リクエストのデータ形式と Content-Type ヘッダー
- サーバー側の定義(
[BindProperty]や[FromBody]などの属性の意味)
リクエストとサーバー側の整合性が取れていないと正しくデータを受信できません。正しくない場合でも実行エラーにはならないケースが多いので、なかなか不整合に気づかないこともあります。モデルバインディングの基本から見ていきます。
Razor Pages のモデルバインディング
基本的なモデルバインディングのコード
フォーム送信と Rzaor Pages のモデルバインディングは以下のようなコードになります。
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>
[BindProperty] 属性が HTTPリクエストの POST データ(フォームの入力値)を Hoge プロパティに自動的にバインド(マッピング)させるための指定になります。
実行すると、このようなページが表示されます(図1)。
デバッグ実行で起動し、フォームに入力して Post ボタンを押すと、OnPost メソッドが呼び出され、Hoge の Name、Age に入力値が設定されていることを確認できます。
HTML
出力された 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 によって、id、name、value の各属性が自動生成されています。
id: モデルのプロパティ名を _ 区切りで変換(Hoge.Name → Hoge_Name)
→ JavaScript やラベルタグ(<label for="...">)などで使用name: モデルバインディング用のフィールド名(Hoge.Name)
→ POST 時にサーバー側で Hoge.Name というプロパティに値をマッピングvalue: モデルの現在値("Hoge")
→ ページ表示時のフォームの初期値
自動生成された name 属性がバインディング時のサーバー側のプロパティ名を指定するキーになります。
- __RequestVerificationToken
ASP.NET Core により自動生成された CSRF 対策用のトークンです。モデルバインディングには直接関係ありませんが、このトークンが無いと無効なリクエストと判定され、サーバー側の処理は実行されず、400 (Bad Request)のレスポンスが返ります。
リクエストデータ
次にリクエストデータの内容を確認してみましょう。
例として Name に "Fuga"、Age に 25 を入力した場合です。
Content-type: application/x-www-form-urlencoded
リクエストボディ: Hoge.Name=Fuga&Hoge.Age=25&__RequestVerificationToken=CfDJ8PYW-uS7dDZDn6LlNPE8uvosZhCcbGyYoNCYQFcq6heF05q5Q6DN3n6FllCtn9wclH7flJVoHqPZBIj-TC9Q0MunNm1mKLy-0E4p6GOU0zcX9pVTjEQgZvWWJzLJpcn60xHjBytC6p38h83g8WYvFN4
Content-type
Content-type は、リクエストボディのデータ形式を示すヘッダー。サーバーはこの指定にもとづきリクエストボディのデータを解釈します。application/x-www-form-urlencoded
長い名称ですが、これはURLエンコード形式またはフォームデータ形式と呼ばれるもので、ブラウザからデータを送信する際の標準的なエンコーディング形式です。データは「キー=値」のペアで表され、各ペアは&記号で連結されます。リクエストボディ
URLエンコード形式の文字列データが送信されています。&記号で分割すると
Hoge.Name=Fuga
Hoge.Age=25
__RequestVerificationToken=CfDJ8PYW-uS7dDZDn6LlNPE8uvosZhCcbGyYoNCYQFcq6heF05q5Q6DN3n6FllCtn9wclH7flJVoHqPZBIj-TC9Q0MunNm1mKLy-0E4p6GOU0zcX9pVTjEQgZvWWJzLJpcn60xHjBytC6p38h83g8WYvFN4
となっています。
キーには html の name 属性が使用されており、この、Hoge.Name が C# のプロパティ名 Hoge.Name に一致することがバインディングされる条件 になります。
fetch
コード
fetch を使用した Ajax 処理に変更してみましょう。
.cs ファイルを変更する必要はありません。
.cshtml ファイルを以下のように修正します。
@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>
button タグの type を button に変更するのを忘れないようにします。デフォルトは submit ですので、button にしないと通常のフォーム送信が行われてしまいます。
リクエストデータ
リクエストデータの内容を確認してみます。
最初の例と同様に Name に "Fuga"、Age に 25 を入力した場合です。
Content-type: multipart/form-data; boundary=----WebKitFormBoundary5BiMG5LpjEL9onVI
リクエストボディ:
------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
FormDataを使用した場合、Content-type にはmultipart/form-dataが自動的に設定されます。1
multipart/form-dataはフォームから複数種類のデータ(ファイルなど)を送信するための Content-type です。この例ではファイル送信は必要ありませんが、FormData を使用するとリクエストボディのデータ形式がmultipart/form-dataになりますので、Content-type はmultipart/form-dataである必要があります。
リクエストボディ
multipart/form-data形式のデータです。boundaryと呼ばれる区切り文字列(この例では「----WebKitFormBoundary5BiMG5LpjEL9onVI」)でデータが区切られています。なぜ受信できるのか?
サーバー側の.csファイルは全く変更していませんが、正しくデータを受信できます。これは[BindProperty]属性がapplication/x-www-form-urlencodedとmultipart/form-dataの両方に対応しているためです。[BindProperty]を付けておけばなんでも受信できるのでは?と思ってしまいますが、そうではありません。次に JSON データを送信する例を見てみます。
fetch(JSON)
コード
送信データの形式を 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}");
}
}
[BindProperty]の使用をやめる- OnPost の引数に
[FromBody] HogeClass hogeを追加
[BindProperty] は JSON データに対応していないので、使用できません。
JSON データを受信するためには、メソッドの引数に [FromBody] 属性を適用するという形式にします。
これで、プロパティではなく、メソッドの引数 hoge にモデルバインディングが行われます。
@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>
.cshtml ファイルの修正点は以下になります。
- リクエストヘッダーに CSRF トークンを設定
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery
@{
var token = antiforgery.GetAndStoreTokens(HttpContext).RequestToken;
}
CSRF token を リクエストヘッダー `RequestVerificationToken` に設定します。通常のフォーム送信や `multipart/form-data` ではリクエストボディに設定されていましたが、JSON データに設定してもトークンとして解釈されないため、ヘッダーに設定する必要があります。
- Content-type に
application/jsonを設定
JSON データを送信するのでapplication/jsonを設定します。指定しなかった場合はデフォルトのtext/plainで送信されます。text/plainで送信した場合、サーバー側で受信することができません(NullReferenceException が発生します)。 - JSON.stringify を使って JavaScript のオブジェクトを JSON 形式の文字列に変換してから body に設定
リクエストボディのデータは文字列である必要があります。オブジェクトのままではサーバー側で受信することができません(NullReferenceException が発生します)。
リクエストデータ
リクエストデータは以下のようになります。
Content-type: application/json
requestverificationtoken: CfDJ8PYW-uS7dDZDn6LlNPE8uvosZhCcbGyYoNCYQFcq6heF05q5Q6DN3n6FllCtn9wclH7flJVoHqPZBIj-TC9Q0MunNm1mKLy-0E4p6GOU0zcX9pVTjEQgZvWWJzLJpcn60xHjBytC6p38h83g8WYvFN4
リクエストボディ: {"Name":"Fuga","Age":25}
$.ajax
次は jQuery の $.ajax() を使用する場合です。
.cs ファイルは最初の例のコードをそのまま使用できます。
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}");
}
}
.cshtml は以下のようになります。
@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>
jQuery の serialize() を使用すると、フォーム要素内のすべての入力値を application/x-www-form-urlencoded 形式の文字列に変換してくれます。
リクエストデータ
Content-type: application/x-www-form-urlencoded
リクエストボディ: Hoge.Name=Fuga&Hoge.Age=25&__Invariant=Hoge.Age&__RequestVerificationToken=CfDJ8PYW-uS7dDZDn6LlNPE8uvqnfpJLTuGH7NvfcuVFLJ5_3LMGcH7tqAUE1RwSyMoFfST7WJmloCyFwehhiYiH2TPxF_dnxSkbWDVXrXqhpJhg7f1QxZwyGSUWUpa24AMxeDtSjqxh-nATdZzHh_oyxzs
$.ajax の Content-type のデフォルトは application/x-www-form-urlencoded のため、明示的に指定する必要はありません。
$.ajax(JSON)
jQuery の $.ajax() を使用しつつ、送信データを JSON にする場合は、どうしたらよいでしょうか?
.cs ファイルは fetch(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}");
}
}
[BindProperty] の使用をやめ、OnPost の引数に [FromBody] HogeClass hoge を追加します。
@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>
}
RequestVerificationTokenの設定contentType: 'application/json'の設定- JSON.stringify を使ってオブジェクトを文字列に変換
必要な修正は fetch(JSON)の場合とほぼ同じになります。
リクエストデータ
Content-type: application/json
Requestverificationtoken: CfDJ8PYW-uS7dDZDn6LlNPE8uvpeLThm5_Z8LiRmjO6nQ5eUodlnqHbI-kjmnqhLB_NuagWlOn4PWSDy_JEAnV9gNVyrhAULTXJe2-JbRbcAWkZCz1blHS68eF-RVG67CAc9nBXEEIqbRGW6V9V48Md2Vvs
リクエストボディ: {"Name":"Fuga","Age":25}
JSON データが送信されていることがわかります。
まとめ
まとめると以下のようになります。
| 方式 | .cs のデータの受け取り方 | Content-Type | 送信データ形式 |
|---|---|---|---|
| フォーム送信 (POST) | [BindProperty] プロパティにバインド |
application/x-www-form-urlencoded | URL エンコード |
| fetch | [BindProperty] プロパティにバインド |
multipart/form-data | マルチパート |
| fetch(JSON) | [FromBody] 引数にバインド |
application/json | JSON |
| $.ajax | [BindProperty] プロパティにバインド |
application/x-www-form-urlencoded | URL エンコード |
| $.ajax(JSON) | [FromBody] 引数にバインド |
application/json | JSON |
繰り返しになりますが、大切なポイントは
- 送信データの形式と Content-Type の指定を合わせること
- 送信データの形式と Content-Type に合わせてサーバー側の実装を変更
になります。
参考になれば幸いです。
参考資料
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",
本文(body)が文字列の場合、Content-Type にはデフォルトでは text/plain が設定されることに留意してください。 そのため、application/json を代わりに送信するために headers オプションを使用しています。
