ASP.NET Core の Razor Pages で削除処理を実装するとき、「OnPostDeleteAsync() の戻り値を object にして独自クラスをそのまま返したい(成功メッセージなど)」と考える場面があります。本記事では、Razor Pages の戻り値がなぜ IActionResult 前提なのかを整理し、AJAX で扱いやすい JsonResult を使った実装パターンを具体例つきで解説します。
「object をそのまま返したい」発想が生まれる場面
削除ボタンを押したらページ遷移せずに一覧から対象行だけ消したい、削除成功ならトースト通知を出したい、といった UI を作るとき、フロント側(JavaScript)は「削除結果を表すデータ(JSON)」を期待します。
このとき C# 側で次のように書けたら楽に見えます。
public async Task<object> OnPostDeleteAsync()
{
return new MySuccessMessage();
}
コントローラー(MVC/Web API)で「オブジェクトを返したら JSON になる」体験があると、Razor Pages でも同じことをしたくなります。しかし Razor Pages のハンドラーは設計思想が少し違い、期待どおりの自動変換にならない(または例外になる)ケースが多いのが実情です。
Razor Pages のハンドラー戻り値は「HTTP 応答」を返す設計
Razor Pages のハンドラーメソッド(OnGet... / OnPost... など)は、最終的には「HTTP 応答として何を返すか」を IActionResult を中心に組み立てる流れになっています。
典型的な戻り値の役割を整理すると次のとおりです。
| 戻り値の型 | 用途 | 実際に返るもの | 向いているケース |
|---|---|---|---|
void / Task | 「ページを描画する」前提 | 同じページの HTML(通常の Razor 描画) | フォーム送信後も同じページを再表示したい |
IActionResult / Task<IActionResult> | HTTP 応答を明示的に制御 | リダイレクト、ページ、JSON、ステータスコードなど | 削除後にリダイレクトしたい、AJAX に JSON を返したい |
object / Task<object> | (Razor Pages 的には想定外) | 自動で JSON にならない/動作が不安定になりやすい | 推奨されない |
ポイントは、Razor Pages は「ページ中心のフレームワーク」で、ハンドラーの戻り値は基本的に IActionResult を返す形に寄せるのが自然、という点です。
なぜ object を返しても「自動で JSON」にならないのか
よく混同されるのが、次の違いです。
- コントローラー(特に Web API):オブジェクトを返すと、出力フォーマッター(JSON など)でシリアライズして返す設計が強い
- Razor Pages:ページ描画(HTML)を基本に、必要に応じて
JsonResult等のIActionResultを返して応答を切り替える設計
つまり、Razor Pages で「オブジェクトを返す=HTTP 応答として JSON を返す」を成立させたいなら、Razor Pages が理解できる形(= IActionResult)にしてあげる必要があります。
結論:AJAX に JSON を返すなら JsonResult を返す
削除の結果(成功メッセージや削除した ID など)を JavaScript で受け取りたい場合は、JsonResult を返すのが王道です。
public async Task<IActionResult> OnPostDeleteAsync()
{
return new JsonResult(new MySuccessMessage());
}
この形なら、クライアントは JSON を受け取り、画面の行を消す・トースト表示をする、といった UI 更新が素直に書けます。
「成功メッセージ用クラス」はこう作ると運用が楽
JSON の形を場当たり的に作ると、フロント側がすぐに複雑になります。削除処理のような「結果だけ返したい」API 風のレスポンスは、最低限次をそろえるのが実務的におすすめです。
| フィールド | 型 | 目的 | 例 |
|---|---|---|---|
success | bool | 成功/失敗を即判定 | true |
message | string | ユーザー向け文言(トースト等) | "削除しました" |
deletedId | int / string | 消す対象を特定 | 123 |
redirectUrl | string(任意) | 必要なら遷移先を返す | "/Items" |
例えば、次のような record(または class)で統一すると、フロント側の条件分岐がシンプルになります。
public record DeleteResponse(bool Success, string Message, int DeletedId, string? RedirectUrl = null);
ハンドラーではこう返します。
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
// 例:削除成功
var res = new DeleteResponse(true, "削除しました。", id);
return new JsonResult(res);
}
ステータスコードも意識すると「バグが減る」
JSON だけ返していると、フロント側が「通信は成功したが処理は失敗」という状態を見落としがちです。API っぽくするなら、ステータスコードも揃えると保守性が上がります。
| 状況 | 推奨ステータス | 返す JSON の例 | フロントの扱い |
|---|---|---|---|
| 削除成功 | 200 | { success: true, message: "削除しました", deletedId: 123 } | 行を削除して通知 |
| ID 不正・入力エラー | 400 | { success: false, message: "入力が不正です" } | メッセージ表示 |
| 対象が存在しない | 404 | { success: false, message: "対象が見つかりません" } | 一覧の再読み込みを検討 |
| 権限がない | 403 | { success: false, message: "権限がありません" } | ログイン/権限導線 |
| サーバー側例外 | 500 | { success: false, message: "削除に失敗しました" } | 汎用エラー表示 |
Razor Pages の PageModel は BadRequest() や NotFound() なども返せるため、JSON と組み合わせて「状態の表現」を統一すると運用が安定します。
実装例:OnPostDeleteAsync で削除して JSON を返す(EF Core 想定)
ここでは「一覧画面で削除 → 成功したら対象行を消す」という想定で、サーバー側の実装例を示します。ポイントは次の 3 つです。
- 戻り値は
Task<IActionResult> - 成功/失敗の JSON 形を揃える
- 必要ならステータスコードも付ける
PageModel(.cshtml.cs)側
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
public class IndexModel : PageModel
{
private readonly AppDbContext _db;
public IndexModel(AppDbContext db)
{
_db = db;
}
public record DeleteResponse(bool Success, string Message, int DeletedId);
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
if (id <= 0)
{
// 400 + JSON
Response.StatusCode = 400;
return new JsonResult(new { success = false, message = "削除対象のIDが不正です。" });
}
var entity = await _db.Items.FirstOrDefaultAsync(x => x.Id == id);
if (entity is null)
{
// 404 + JSON
Response.StatusCode = 404;
return new JsonResult(new { success = false, message = "削除対象が見つかりませんでした。" });
}
_db.Items.Remove(entity);
try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
// 409(競合)なども選択肢
Response.StatusCode = 409;
return new JsonResult(new { success = false, message = "他のユーザー操作と競合しました。最新状態を確認してください。" });
}
catch
{
Response.StatusCode = 500;
return new JsonResult(new { success = false, message = "削除に失敗しました。もう一度お試しください。" });
}
// 200 + JSON
return new JsonResult(new DeleteResponse(true, "削除しました。", id));
}
}
この例では Response.StatusCode を設定していますが、StatusCode(...) 系の結果や NotFound() を返すやり方でも構いません。大事なのは「フロントが失敗を判定できること」と「ユーザー向けメッセージが返ること」です。
Razor(.cshtml)側:削除ボタンとハンドラー呼び出しの作り方
Razor Pages では、OnPostDeleteAsync を呼ぶために、一般的に asp-page-handler="Delete" を使います。非AJAXなら通常のフォームでOKですが、AJAX化するなら「URL さえ分かれば fetch で呼べる」状態にしておくのがコツです。
例えば一覧の各行に削除ボタンを置くなら、次のようにデータ属性に ID を埋め込むと JavaScript が扱いやすくなります。
<!-- 例:一覧の各行に削除ボタンを配置 -->
<button type="button" class="js-delete" data-id="123">削除</button>
<!-- CSRF 対策:トークンをフォームか hidden に出しておく(後述) -->
<form id="antiforgeryForm" method="post"></form>
Razor Pages のフォーム送信では、通常はフレームワークが隠しトークンを埋め込む構成が多いです。AJAX で POST を投げる場合は、そのトークンをヘッダーなどで一緒に送るのが重要になります。
JavaScript 側:fetch で OnPostDeleteAsync を呼び、行を消す
削除は「GET 以外の操作」なので、CSRF 対策(アンチフォージェリトークン)が絡みます。プロジェクトの設定次第で細部は異なりますが、典型的には次のような流れになります。
- ページ内にある
__RequestVerificationToken(hidden)を取り出す - POST リクエストにトークンを載せる
?handler=Deleteを付けてハンドラーを指定
例として、フォームデータで id を送るパターンです。
async function deleteItem(id) {
// hidden input から token を取る例(実際の配置に合わせて取得)
const tokenInput = document.querySelector('input[name="__RequestVerificationToken"]');
const token = tokenInput ? tokenInput.value : '';
const form = new URLSearchParams();
form.append('id', String(id));
const res = await fetch(`?handler=Delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'RequestVerificationToken': token
},
body: form.toString()
});
// まず HTTP として成功かを確認(400/404/500 ならここで落とせる)
if (!res.ok) {
let err;
try { err = await res.json(); } catch { err = null; }
const msg = err?.message ?? `削除に失敗しました(HTTP ${res.status})`;
alert(msg);
return;
}
const data = await res.json();
if (!data.success) {
alert(data.message ?? '削除に失敗しました。');
return;
}
// 成功:画面から行を消す(例)
const row = document.querySelector(`[data-row-id="${data.deletedId}"]`);
if (row) row.remove();
// 通知(例)
console.log(data.message);
}
document.addEventListener('click', (e) => {
const btn = e.target.closest('.js-delete');
if (!btn) return;
const id = Number(btn.dataset.id);
if (!Number.isFinite(id) || id <= 0) return;
if (!confirm('本当に削除しますか?')) return;
deleteItem(id);
});
ここで ?handler=Delete としているのは、Razor Pages のハンドラー名(OnPostDeleteAsync の Delete 部分)を指定しているためです。URL の構成をこの形に揃えると、フロント側は「ページをAPIっぽく呼ぶ」感覚で実装できます。
「削除処理の結果を受け取って画面を更新したい」なら JSON が自然な理由
ページ遷移を前提にすると、削除成功後のメッセージ表示は TempData などを使って「次のページ表示時に出す」作りになりがちです。一方、AJAX 前提の UI では「その場で受け取って、その場で描画を変える」必要があります。
それぞれの向き・不向きを整理すると、判断が速くなります。
| やりたいこと | おすすめの返し方 | メリット | 注意点 |
|---|---|---|---|
| 削除後に一覧へ戻したい | RedirectToPage(...) | 実装が単純、JS が不要 | 部分更新には不向き |
| 削除後も同じページで表示更新したい | new JsonResult(...) | 部分更新しやすい、UX が良い | CSRF 対策・フロント実装が必要 |
| 同じ削除APIを複数画面・外部からも使いたい | API(Controller / Minimal API)に切り出し | 責務分離、再利用しやすい | 設計が増える(認証/認可など) |
非AJAXで「成功メッセージ」を返したい場合の王道(TempData)
「オブジェクトを返したい」の動機が、実は「成功メッセージを出したい」だけなら、AJAX ではなく画面遷移(PRG: Post-Redirect-Get)を使うほうが自然なこともあります。
例えば削除後に一覧へ戻す場合、次のように TempData を使うと、リロードや二重送信にも強くなります。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
public class IndexModel : PageModel
{
[TempData]
public string? FlashMessage { get; set; }
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
// ...削除処理...
FlashMessage = "削除しました。";
return RedirectToPage("./Index");
}
}
.cshtml 側は次のように表示するだけです。
<!-- 例:メッセージ表示 -->
@if (!string.IsNullOrEmpty(Model.FlashMessage))
{
<div class="notice">@Model.FlashMessage</div>
}
この方法なら「成功メッセージ用のオブジェクトを返す」必要がそもそもありません。用途がページ遷移中心なら、こちらのほうが保守しやすいことが多いです。
よくある落とし穴と、実務で効くチェックポイント
Razor Pages で JSON を返す実装はシンプルですが、現場では次のミスが頻出します。先に潰しておくと、デバッグ時間が一気に減ります。
| 症状 | ありがちな原因 | 対処 |
|---|---|---|
| ハンドラーが呼ばれない | ?handler=Delete を付けていない/asp-page-handler が違う | URL とメソッド名の対応(OnPostDeleteAsync の Delete)を再確認 |
| 400 が返る(CSRF 関連) | アンチフォージェリトークンが送れていない | hidden の __RequestVerificationToken を取得し、ヘッダーへ設定 |
| JSON のプロパティ名が想定と違う | シリアライズ設定で camelCase になっている等 | フロント側を合わせるか、AddJsonOptions で方針を統一 |
| 削除成功なのに UI が更新されない | フロントが deletedId を見ていない/DOM の特定が曖昧 | 行の要素に data-row-id を付け、確実に一致させる |
| 例外時に HTML が返ってきて JSON 解析で落ちる | 例外時に開発用エラーページ(HTML)が出ている | サーバー側で try/catch し JSON を返す、フロント側で res.ok を先に見る |
JSON の命名規則を統一する(camelCase か PascalCase か)
フロントとバックの境界で、地味に効いてくるのが命名規則です。C# は PascalCase が自然ですが、JavaScript では camelCase が一般的です。
プロジェクト全体で統一したい場合は、起動時の設定で System.Text.Json のオプションを揃えます(例)。
using System.Text.Json;
builder.Services
.AddRazorPages()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
こうしておけば、レスポンスのプロパティ名がフロントで扱いやすくなります。逆に PascalCase のまま返す方針にするなら、フロント側でその前提に合わせるだけです。どちらでもいいので、途中で混在させないのが重要です。
「削除に成功したのに、実は削除されていない」を防ぐ
削除処理で怖いのは、フロントは成功表示したのに DB では削除が失敗していた、逆に DB は削除できたのに UI が更新されずユーザーが再操作してしまう、といったズレです。
実務では次の 2 点を意識すると安定します。
- サーバー側:削除結果(成功/失敗)を必ず JSON で返す。例外も握りつぶさず「失敗」を返す
- フロント側:
res.ok(HTTP)とdata.success(業務結果)の両方で判定する
この二段構えにするだけで「表示は成功なのに実態が違う」事故が減ります。
まとめ:Razor Pages で「オブジェクトをそのまま返したい」なら、返すべきは IActionResult
Razor Pages のハンドラーで object を返して自動的に JSON にする、という使い方は基本的に噛み合いません。Razor Pages はページ中心の設計で、戻り値は IActionResult を返して HTTP 応答を明示するのが前提だからです。
削除処理(OnPostDeleteAsync)の結果をクライアント側で受け取り、画面を更新したいなら、次の形に寄せるのが最も堅実です。
- 戻り値は
Task<IActionResult> - AJAX 用の応答は
new JsonResult(...) - 必要ならステータスコードも揃える(400/404/409/500 など)
- レスポンスの形(success/message/id)を統一してフロントを簡単にする
この方針で作ると、「削除成功時の行削除」「失敗時のメッセージ表示」「競合や権限エラーの扱い」まで一貫した実装になり、Razor Pages でも快適に AJAX ベースの UI を構築できます。

コメント