ASP.NET CoreでIFormCollectionをTempData+JSONで別コントローラーへ渡す方法(DTOで安全に値を取り出す)

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 を埋めて再 POSTConfirm(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 なども検討する

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次