Razor ページのフォーム送信後、POST で受け取った IFormCollection を TempData に入れて RedirectToAction() で別コントローラーへ渡したい――ASP.NET Core でよくある悩みです。解決の近道は、IFormCollection を丸ごと保存せず、必要な値だけを DTO に詰めて JSON として受け渡すことです。
よくある状況:POST で受け取った IFormCollection を別アクションに渡したい
ASP.NET Core(MVC / Razor Pages)では、フォームを送信すると POST アクション(または Razor Pages の OnPost)で入力値を受け取れます。急いで実装すると、引数を IFormCollection collection にして「とりあえず全部受け取る」形になりがちです。
ところが、POST の中でそのまま画面を返すのではなく、いわゆる PRG パターン(Post-Redirect-Get)で RedirectToAction() して別コントローラーに遷移したい場面があります。たとえば次のようなケースです。
- 確認画面を GET で表示したい(リロードで二重送信を防ぐ)
- 登録処理の前に別コントローラーでチェックや整形をしたい
- 処理を担当するコントローラーを分けたい
このとき、「POST で受け取った IFormCollection を TempData に入れて、遷移先で取り出す」発想は自然です。しかし TempData["Param"] = JsonConvert.SerializeObject(collection) のように IFormCollection を丸ごと JSON 化しようとすると、遷移先での復元に失敗することが多く、欲しい項目(Title / Sections / EmployeeName / Locations)だけを安全に取り出せなくなります。
なぜ IFormCollection をそのまま JSON で復元できないのか
結論から言うと、IFormCollection は「HTTP リクエストのフォーム値を参照するためのインターフェース」であり、シリアライズして持ち運ぶ前提の型ではありません。特に次の理由で、JsonConvert.DeserializeObject で IFormCollection として復元するのは現実的ではありません。
| つまずきポイント | 内容 | 起きやすい症状 |
|---|---|---|
| インターフェース型である | IFormCollection は interface のため、JSON から「どの具象型に戻すか」を自動的に決められません。 | デシリアライズ時に例外が出る/null になる |
| 内部に複雑な要素を含む | フォームにはファイル(IFormFile)が含まれることがあり、IFormCollection 側もファイルコレクションを持ちます。 | 想定外に巨大化する/復元できない |
| 値の型が素直な string ではない | 値は StringValues(複数値を扱える構造)で保持されます。JSON にすると形が崩れやすく、復元先の型が曖昧になります。 | 配列なのか単一なのかが分からない/値が欠落する |
| そもそも「元に戻す必要」がない | 欲しいのはコレクションそのものではなく、Title / Sections / EmployeeName / Locations の値です。 | 実装が過剰に複雑化する |
つまり、復元しやすい形に変換してから TempData に入れるのが正攻法です。フォームの全内容を「なんでも入れられる箱」として扱うのではなく、次の画面で必要なデータだけを設計して渡すと、バグが減り、保守性も上がります。
最も安全でシンプルな解決策:必要な項目だけ DTO に詰めて TempData に保存する
今回取り出したいのは次の 4 項目です。
- Title(単一値)
- Sections(複数値になりやすい)
- EmployeeName(単一値)
- Locations(複数値になりやすい)
この 4 つだけを持つ「受け渡し専用のモデル(DTO)」を作り、その DTO を JSON として TempData に入れます。遷移先は DTO としてデシリアライズするだけなので、読みやすく安全です。
受け渡し用モデル(DTO)の例
Sections と Locations はチェックボックスや複数選択の可能性が高いため、List<string> または string[] が扱いやすいです。
public class FormDataModel
{
public string Title { get; set; } = "";
public List<string> Sections { get; set; } = new();
public string EmployeeName { get; set; } = "";
public List<string> Locations { get; set; } = new();
}
POST 側:IFormCollection から必要な 4 項目だけ取り出して JSON 化
IFormCollection の値は StringValues なので、単一値は .ToString()、複数値は .ToArray() で扱うと分かりやすくなります。キーが存在しない場合も想定し、TryGetValue で安全に取り出すのがおすすめです。
[HttpPost]
public IActionResult Post(IFormCollection collection)
{
var formData = new FormDataModel
{
Title = collection.TryGetValue("Title", out var title)
? title.ToString()
: "",
Sections = collection.TryGetValue("Sections", out var sections)
? sections.ToArray().ToList()
: new List<string>(),
EmployeeName = collection.TryGetValue("EmployeeName", out var employeeName)
? employeeName.ToString()
: "",
Locations = collection.TryGetValue("Locations", out var locations)
? locations.ToArray().ToList()
: new List<string>(),
};
// JSON 文字列として TempData に保存
TempData["Param"] = JsonConvert.SerializeObject(formData);
// PRG パターン:POST の結果は Redirect で返す
return RedirectToAction("Confirm", "Next");
}
ここでのポイントは、TempData に入れるのは「復元しやすい DTO の JSON」だけということです。IFormCollection を直接持ち運ぶよりも、遷移先での取り出しが圧倒的に安定します。
遷移先:DTO としてデシリアライズして個別取得
遷移先では、TempData から JSON を取り出して DTO に戻します。TempData は直接アクセスすると消費される(次リクエストで消える)点に注意しつつ、まずは null ガードを入れておくと事故が減ります。
public IActionResult Confirm()
{
var json = TempData["Param"]?.ToString();
if (string.IsNullOrWhiteSpace(json))
{
// 直接 URL を開かれた/戻るボタン等で TempData が無いケース
return RedirectToAction("Index", "Home");
}
var formData = JsonConvert.DeserializeObject<FormDataModel>(json);
if (formData == null)
{
return RedirectToAction("Index", "Home");
}
// 個別取得
var title = formData.Title;
var sections = formData.Sections;
var employeeName = formData.EmployeeName;
var locations = formData.Locations;
return View(formData);
}
View 側は FormDataModel を受け取って表示すれば OK です。Sections / Locations は配列やリストなので、そのままループできます。
Sections / Locations のような「複数値」を取り出すときのコツ
IFormCollection の値は「単一値でも複数値でも同じキーで届く」ことがあります。特にチェックボックス群や複数選択の <select multiple> は、同じ name で複数値が送られます。
| フォーム部品の例 | 送信される値 | IFormCollection での取り出し |
|---|---|---|
| 単一の input(テキストなど) | 1 つの値 | collection["Title"].ToString() |
| チェックボックス複数(同じ name) | 複数の値 | collection["Sections"].ToArray() |
| select multiple | 複数の値 | collection["Locations"].ToArray() |
| チェックされていない | キー自体が来ないことがある | TryGetValue で存在確認し、空配列/空リストを入れる |
複数値の項目を string にしてしまうと、後から「カンマ区切りで結合されていた」などの曖昧さが生まれます。DTO の段階で List<string> にしておけば、後工程(確認画面表示、検証、DB 登録)で迷いがなくなります。
さらに実務的に:最初から DTO を受け取る(モデルバインディング)
本当におすすめなのは、POST の引数を IFormCollection にせず、最初から FormDataModel を受け取ることです。ASP.NET Core のモデルバインディングを使えば、フォームの name とプロパティ名を合わせるだけで自動で詰めてくれます。
[HttpPost]
public IActionResult Post(FormDataModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
TempData["Param"] = JsonConvert.SerializeObject(model);
return RedirectToAction("Confirm", "Next");
}
これにより、POST 側で「取り出しロジック」を持たなくて済みます。値の型もはっきりし、リファクタリングしやすくなります。
フォーム側の name を揃える
Razor(.cshtml)では asp-for を使うと name のズレを防げます。チェックボックスや複数選択は同じ name を意識します。
<input type="text" name="Title" />
Sales
HR
Tokyo
Osaka
バリデーションも自然に効く
DTO にデータアノテーションを付けると、ModelState で一括検証できます。TempData に詰める前に弾けるので、確認画面での不整合も減ります。
public class FormDataModel
{
[Required]
public string Title { get; set; } = "";
public List<string> Sections { get; set; } = new();
[Required]
public string EmployeeName { get; set; } = "";
public List<string> Locations { get; set; } = new();
}
実装を整理する小技:TempData の JSON 化を共通化する
コントローラーが増えると、TempData["Param"] = JsonConvert.SerializeObject(...) と DeserializeObject が散らばりがちです。プロジェクトの方針として Newtonsoft.Json を使うなら、拡張メソッドで「型付き TempData」を用意すると読みやすくなります。
public static class TempDataExtensions
{
public static void SetJson<T>(this ITempDataDictionary tempData, string key, T value)
{
tempData[key] = JsonConvert.SerializeObject(value);
}
public static T? GetJson<T>(this ITempDataDictionary tempData, string key)
{
var json = tempData[key]?.ToString(); // ここで消費される点に注意
return string.IsNullOrWhiteSpace(json)
? default
: JsonConvert.DeserializeObject<T>(json);
}
public static T? PeekJson<T>(this ITempDataDictionary tempData, string key)
{
var json = tempData.Peek(key)?.ToString(); // 消費しない
return string.IsNullOrWhiteSpace(json)
? default
: JsonConvert.DeserializeObject<T>(json);
}
}
これでコントローラー側は次のようにスッキリします。
TempData.SetJson("Param", model);
var formData = TempData.PeekJson("Param");
TempData を使うときの注意点
TempData は便利ですが、「いつでもどこでも使える万能ストレージ」ではありません。フォーム値の受け渡しに使うなら、挙動と制約を押さえておくと安心です。
基本は「次の 1 リクエストだけ」
TempData は読み出すと消費されるのが基本です。確認画面の表示後にもう一度同じデータを参照したい場合は、Peek / Keep を使います。
// 消費せずに読む
var json = TempData.Peek("Param")?.ToString();
// 読んだあとに残す(次のリクエストでも使う)
TempData.Keep("Param");
ただし、Peek/Keep を多用すると「いつ消えるのか分からないデータ」が増えます。画面遷移が増えるほど破綻しやすいので、あくまで短距離の受け渡しに限定するのがコツです。
Cookie ベースの TempData はサイズに注意
ASP.NET Core の TempData は、既定では Cookie を使って保存されます(プロジェクト設定により Session ベースに変えることも可能です)。Cookie はブラウザー制約でサイズ上限が厳しく、JSON が大きいと保存できずに値が欠落します。
- Title / EmployeeName のような短い文字列なら問題になりにくい
- Sections / Locations が大量、または自由入力が長文だと危険
- 機密情報や個人情報を安易に入れない(保存先はクライアント側)
「フォーム全体を TempData に入れる」発想は、サイズ・セキュリティ・保守性の面で負債になりやすいので、今回のように 必要な項目だけ DTO にして最小化するのが最も実務的です。
確認画面の次がある場合の設計例
確認画面のあとに「確定ボタン」で最終 POST を行うケースでは、TempData をどう扱うかで迷いがちです。代表的な設計を整理すると判断が早くなります。
| 設計 | 流れ | メリット | デメリット |
|---|---|---|---|
| TempData を Keep して使い回す | POST → Redirect → Confirm(GET) → Final(POST) | 実装が簡単 | 画面遷移が増えると管理が難しい、失効・二重タブに弱い |
| 確認画面に hidden を埋めて再 POST | Confirm(GET) で DTO を表示し、hidden で同じ値を保持して Final(POST) | TempData 依存が減る、ブラウザー操作に比較的強い | hidden の量が増える、改ざん対策(検証)が必要 |
| サーバー側に一時保存してトークン参照 | POST で保存 → トークンを TempData/URL に載せる → Confirm/Final はトークンで取得 | 大きなデータに強い、複数画面に跨って安定 | 実装コスト、クリーンアップ設計が必要 |
「Title / Sections / EmployeeName / Locations を確認して確定する」程度なら、hidden を使った再 POST も相性が良いです。最終的にサーバーで必ず検証(ModelState、業務ルール)を行う前提なら、改ざんのリスクも制御できます。
受け渡し手段の比較:TempData がベストとは限らない
要件によっては TempData より適した手段があります。選定の目安を表にまとめます。
| 方法 | 向いているケース | 注意点 |
|---|---|---|
| TempData(DTO を JSON) | 次の 1 画面だけに値を渡す/確認画面など | サイズ制限、読み出しで消える、複数画面に跨ると管理が難しい |
| クエリ文字列(RedirectToAction の route values) | 短い単一値を渡す(ID、フラグ、ページ番号など) | URL に露出する、複数値・長文に不向き |
| Session / 分散キャッシュ | 複数画面で保持したい、サイズが大きい | 破棄タイミング設計が必要、サーバーリソース消費 |
| DB に一時保存(トークンで参照) | 確実に残したい、監査・履歴が必要、複数端末でも続きから | 実装コスト、クリーンアップ設計が必要 |
「Title / Sections / EmployeeName / Locations を確認画面に出したい」程度なら、DTO + TempData で十分なことが多いです。一方で、画面が 3 つ以上連続する、入力途中保存が必要、データ量が大きい、といった要件があるなら Session や DB の方が後悔しにくいこともあります。
ファイル(IFormFile)を渡したい場合は別設計にする
IFormCollection を丸ごと渡したい動機の中には「ファイルも含めて別画面で扱いたい」が含まれている場合があります。しかしファイル(IFormFile)は TempData に入れて持ち運ぶ設計に向きません。
- POST で受け取ったら、まずサーバー側の一時領域に保存する
- TempData にはファイルパスやトークン(GUID など)だけを入れる
- 確認画面でトークンを使ってファイルを参照する
こうすることで、Cookie サイズ制限やシリアライズ問題を避けつつ、画面分割も安全にできます。
よくあるつまずきと解決チェックリスト
DTO + TempData にしても、運用でよくハマるポイントがあります。症状から逆引きできるようにまとめます。
| 症状 | 原因 | 対処 |
|---|---|---|
遷移先で TempData["Param"] が null | 直接 URL アクセス/戻るボタン/別タブで開いたなどで TempData が既に消費された | null ガードを入れてリダイレクト、必要なら Peek/Keep を検討 |
| Sections / Locations が空になる | フォーム側の name が違う/チェックされていない場合はキーが来ない | name を揃える、TryGetValue で存在チェックし空リストを入れる |
| JSON デシリアライズで例外 | TempData に想定外の値が入った、または JSON が壊れている | TempData のキーを分ける、DTO を固定し、例外時は安全に戻す導線を用意 |
| 長文入力で値が欠落する | Cookie ベース TempData のサイズ制限に引っかかった可能性 | TempData に入れる項目を減らす/Session へ切り替える/DB 保存を検討 |
| 遷移先で値は取れるが、もう一度表示できない | TempData は読み出しで消費される | 表示後に再利用するなら Keep、参照だけなら Peek |
まとめ
IFormCollectionはシリアライズして持ち運ぶ型ではなく、丸ごと TempData に保存して復元しようとすると失敗しやすい- 必要な項目(Title / Sections / EmployeeName / Locations)だけを持つ DTO を作り、JSON 化して TempData に入れると安定する
- さらに良いのは、POST の引数を最初から DTO にしてモデルバインディングで受け取る設計
- TempData は「次の 1 リクエストだけ」「サイズ制限あり」が基本。長距離の受け渡しには Session / DB なども検討する

コメント