ASP.NET MVC RedirectToActionでモデルを渡す?TempDataよりID参照が安全な理由と404対策

ASP.NET MVCで注文サマリー(ordersum)などのモデルを作成したあと、別Controller/ActionへRedirectToActionで遷移して処理を続けたい——この要件はECや申込フローでよく出ます。本記事では「オブジェクト丸ごと渡し」が非推奨な理由、TempDataの正しい使いどころ、404が出る典型原因と対策を、具体的なコードで整理します。

目次

RedirectToActionで「モデル(オブジェクト)全体」を渡したくなる背景

コントローラーで注文サマリー(OrderSummary / ordersum)のようなオブジェクトを組み立てた直後に、確認画面や決済画面へ進めたくなるのは自然です。特に、以下のような流れでは「次のActionにそのまま渡したい」と考えがちです。

  • カート → 注文情報入力 → 注文サマリー作成 → 確認画面 → 注文確定
  • 申込入力 → 見積サマリー作成 → 確認画面 → 申込確定

しかし、ここで重要なのは「Redirect(リダイレクト)=別リクエスト」という性質です。RedirectToActionはサーバー内部で次のActionへ“移動”するのではなく、ブラウザに「次はこのURLへ行ってください(302/303)」と命令し、ブラウザが改めてGET等を投げ直します。つまり、メモリ上のordersumをそのまま持ち越す設計と相性が悪いのです。

まず押さえたい:ViewとRedirectは似て非なるもの

「画面を表示する」という結果だけ見ると似ていますが、実際には仕組みが違います。ここを誤解すると、オブジェクト受け渡しで詰まりやすくなります。

項目return View(…)RedirectToAction(…)
リクエスト回数1回(同一リクエスト内)2回(リダイレクト後に新規リクエスト)
ブラウザのURL変わらない(基本)変わる(遷移先URLになる)
モデルの受け渡しそのまま渡せる基本は渡せない(URLに載る範囲のみ)
代表用途入力エラー時の再表示、同一画面描画POST後に確認画面へ、完了画面へ(PRG)

結論として、Redirectで「オブジェクト丸ごと」を渡す発想自体が、HTTPの仕組みに逆らいやすいのが根本原因です。

結論:Redirectでオブジェクト丸ごとは基本的に非推奨

RedirectToActionのrouteValuesにordersumのようなオブジェクトを渡すと、フレームワークはプロパティを展開し、最終的にはクエリ文字列(またはルートセグメント)として表現できるものだけがURLに反映されます。

この方法は、次の理由で壊れやすく、運用上も危険です。

  • URL長の制限にすぐ当たる(商品明細や住所・氏名・金額などが増えると顕著)
  • リストやネスト構造など、複雑なオブジェクトの完全再現が難しい
  • 機密情報(氏名・住所・金額・クーポン・内部計算値など)がURLに載るリスクがある
  • ブラウザ履歴、アクセスログ、リファラ、解析ツール等に残りやすい
  • 仕様変更(プロパティ追加/変更)でURLが肥大化して突然壊れる

そのため、一般的なベストプラクティスは次の形になります。

王道のMVCパターン:保存してIDだけ渡す(PRG: Post-Redirect-Get)

注文サマリーのようなものは「画面遷移のための一時データ」に見えても、実際には業務データの核に近い情報を含みます。そこで、PRG(Post-Redirect-Get)に沿って、サーバー側に保存し、参照キー(ID/トークン)だけをURLで渡すのが定石です。

  • 最初のアクション(POSTなど)でordersumを生成
  • DB / 分散キャッシュ / サーバー側ストアに保存
  • 参照キー(例:Guidのトークン、orderDraftIdなど)を発行
  • RedirectToActionではIDだけ渡す
  • 遷移先(GET)でIDから再取得して表示・処理

このやり方のメリットは、単に「渡せる」だけではありません。

  • URLが短く安定し、仕様変更にも強い
  • セキュリティと監査性が高い(ログに機微情報が残りにくい)
  • スケールアウト(複数台構成)でも破綻しにくい
  • 「戻る」「複数タブ」など実ユーザー操作でも崩れにくい

実装例:注文サマリーを作って確認画面へ遷移する

ここでは「注文サマリーを作るPOST → 確認画面のGET」という流れを、IDだけ渡す形で実装します。サンプルはASP.NET Core MVC風に書きますが、MVC 5でも考え方は同じです。

データ構造の例(注文サマリーはDTOとして保持)

画面表示用・確認用のデータとしてまとめたDTO(またはViewModel)を想定します。

public class OrderSummaryDto
{
    public string CustomerId { get; set; } = "";
    public decimal Subtotal { get; set; }
    public decimal Tax { get; set; }
    public decimal ShippingFee { get; set; }
    public decimal Total { get; set; }

    public List<OrderItemDto> Items { get; set; } = new();
}

public class OrderItemDto
{
    public string Sku { get; set; } = "";
    public string Name { get; set; } = "";
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

保存先の考え方:DBかキャッシュか

「注文サマリーは確定前だからDBに入れたくない」というケースもあります。その場合は、DBに永続化する代わりに、分散キャッシュ(Redis等)やサーバー側ストアに短い有効期限で保存するとバランスが取れます。重要なのは、URLにモデル本体を載せないことです。

方式渡すもの保存場所向くケース注意点
ID参照(おすすめ)orderDraftId / tokenDB確定まで長い、監査したい、再開したい設計とテーブルが必要
ID参照(軽量)token分散キャッシュ(Redis等)短命な確認画面、トラフィックが多いTTL設計、再取得失敗時のUX
TempData小さな値(メッセージ等)フレームワーク依存一度きりの通知、フラッシュメッセージ大きい/重要データに不向き
Sessionキー or 小規模データサーバー側(または外部)一時的な状態管理スケールアウト時の構成注意
クエリ文字列プリミティブURL検索条件、ページ番号など機微情報は載せない

コントローラー例:POSTで下書きを作り、GETへリダイレクト

ポイントは、POST側でordersum(OrderSummaryDto)を保存してtokenを返すこと、RedirectToActionではtokenだけ渡すことです。

using System.Text.Json;
using Microsoft.AspNetCore.Mvc;

public class CheckoutController : Controller
{
    private readonly IOrderSummaryService _summaryService;
    private readonly IOrderDraftStore _draftStore;

    public CheckoutController(IOrderSummaryService summaryService, IOrderDraftStore draftStore)
    {
        _summaryService = summaryService;
        _draftStore = draftStore;
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> BuildSummary(OrderInputViewModel input)
    {
        // 1) サマリーを作る(計算・割引・税・送料など)
        var summary = await _summaryService.BuildAsync(User, input);

        // 2) サーバー側に保存して、参照用トークンを発行
        var token = Guid.NewGuid();
        await _draftStore.SaveAsync(token, User, summary, TimeSpan.FromMinutes(30));

        // 3) リダイレクトではトークンだけ渡す
        return RedirectToAction(nameof(Confirm), "Checkout", new { token });
    }

    [HttpGet]
    public async Task<IActionResult> Confirm(Guid token)
    {
        // 4) 遷移先でトークンから再取得
        var summary = await _draftStore.GetAsync(token, User);
        if (summary == null)
        {
            // 期限切れ・別タブ上書き・不正トークンなど
            return NotFound();
        }

        return View(summary);
    }
}

この形にすると、「確認画面のURLをブックマークした」「戻るボタンを押した」「画面更新した」といった実運用でよくある操作にも、ある程度耐性が出ます(期限切れ時のハンドリングは必要です)。

下書きストア例:分散キャッシュで30分だけ保持する

DBではなくキャッシュに置く場合の考え方です。重要なのは、キーをユーザーに紐付けて、他人のtokenで参照できないようにすることです(tokenが推測困難でも、念のため二重に縛ると安全です)。

public interface IOrderDraftStore
{
    Task SaveAsync(Guid token, ClaimsPrincipal user, OrderSummaryDto summary, TimeSpan ttl);
    Task<OrderSummaryDto?> GetAsync(Guid token, ClaimsPrincipal user);
}

public class DistributedCacheOrderDraftStore : IOrderDraftStore
{
    private readonly IDistributedCache _cache;

    public DistributedCacheOrderDraftStore(IDistributedCache cache)
    {
        _cache = cache;
    }

    private static string BuildKey(Guid token, ClaimsPrincipal user)
    {
        // 例:ユーザーIDと結合して「他人のtoken」参照を防ぐ
        var userId = user.FindFirst("sub")?.Value
                  ?? user.FindFirst(ClaimTypes.NameIdentifier)?.Value
                  ?? "anonymous";
        return $"orderDraft:{userId}:{token}";
    }

    public async Task SaveAsync(Guid token, ClaimsPrincipal user, OrderSummaryDto summary, TimeSpan ttl)
    {
        var key = BuildKey(token, user);
        var json = JsonSerializer.Serialize(summary);

        await _cache.SetStringAsync(
            key,
            json,
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = ttl
            });
    }

    public async Task<OrderSummaryDto?> GetAsync(Guid token, ClaimsPrincipal user)
    {
        var key = BuildKey(token, user);
        var json = await _cache.GetStringAsync(key);

        if (string.IsNullOrWhiteSpace(json))
            return null;

        return JsonSerializer.Deserialize<OrderSummaryDto>(json);
    }
}

これならRedirectToActionで渡すのはGuidだけで済み、サマリーの中身がどれだけ増えてもURLは安定します。

TempDataはベストプラクティス?結論:用途を選べば便利、主役にはしない

TempDataは「次のリクエストまで」値を受け渡すための仕組みで、フラッシュメッセージのような用途では非常に便利です。一方で、注文サマリーのような重要かつ大きくなりがちなオブジェクトを渡す主役にするのはおすすめしません。

TempDataが向いているケース

  • 「保存しました」「エラーが発生しました」など一度だけ表示したい通知
  • 一覧画面へ戻ったときに選択していたタブを保持したい
  • リダイレクト後に軽い状態を少しだけ引き継ぎたい

TempDataが向いていないケース

  • 商品明細を含む注文サマリーなど、サイズが膨らむデータ
  • 金額・割引・送料計算など、整合性が重要なデータ
  • 個人情報・決済に関わる情報など、機密性が高いデータ

TempDataの「保存先」はフレームワークで違う

TempDataが何に載るかは環境で変わります。ここを知らずに「入ったからOK」と判断すると、後で事故りやすいです。

環境TempDataの既定実務上の注意
ASP.NET MVC 5(System.Web)Sessionベースが一般的InProcならメモリ圧迫、Out-of-procならシリアライズ要件が出る
ASP.NET Core MVCCookieベースが既定になりやすいサイズ制限が厳しい。大きいデータを入れると肥大化しやすい
ASP.NET Core MVC(Session TempDataに変更)SessionベースCookie肥大化は回避できるが、セッション運用とスケール設計が必要

つまり、TempDataは「便利な小道具」ですが、注文サマリーのような業務の核データを運ぶトラックとして使うと、サイズ・整合性・セキュリティ・スケールのどこかで無理が出やすいということです。

TempDataの良い使い方(例:完了メッセージだけ渡す)

TempData["FlashMessage"] = "注文内容を更新しました。";
return RedirectToAction(nameof(Confirm), new { token });

受け渡しは最小限にし、重要データはサーバー側ストアで管理するのが安定します。

「どうしても一時的に持ち回りたい」場合の現実的な選択肢

プロジェクト事情により「まだDB設計がない」「確認画面までの短い間だけ保持したい」ということもあります。その場合は、次のように“Redirectでオブジェクト丸ごと”以外の道を選ぶのが現実的です。

選択肢概要向く状況注意点
Redirectしない(Viewで返す)同一リクエスト内でそのまま確認ビューを描画単純な確認画面、PRG不要または別で担保できる再読み込みで再POSTになりうる(PRGの利点が減る)
分散キャッシュに保存してtoken渡し短いTTLで保持し、URLはtokenのみ短命データ、スケールアウト前提期限切れ時の導線が必要
Sessionに保存してキー渡しユーザーセッションに紐付けて保存単一サーバー/小規模構成セッション管理、複数タブ上書きに注意
隠しフィールドでPOST継続確認画面に値を埋めて次POSTへ小さな入力値の再送改ざん対策が必須。機微情報には不向き

注文金額や割引など改ざんされたら困る値をクライアント側に持たせる場合は、必ずサーバー側で再計算・再検証してください。確認画面で見せる値と、確定時に採用する値は「同じに見える」だけで、確定時は必ずサーバーが正とするのが安全です。

RedirectToActionで404が出る“ありがち”原因

「リダイレクトしたら404」という相談は、原因がいくつかのパターンに収束します。まずは“本当にルーティングで404なのか”、それとも“Action内でNotFoundを返している404なのか”を切り分けるのが近道です。

特に多い:引数の順番が逆

RedirectToActionの基本形は次の順番です。

RedirectToAction(actionName, controllerName, routeValues)

例えば、次のように書くのが一般的です(controllerNameは末尾のControllerを省略します)。

return RedirectToAction("MyAction", "MyController", new { id = orderSumId });

これが、もし次のようにcontroller/actionを逆に渡していると、存在しないActionに飛んで404になりがちです。

// NG例:順番が逆
return RedirectToAction("MyController", "MyAction");

文字列指定はタイプミスもしやすいので、同一コントローラー内ならnameofを使うと安全です。

return RedirectToAction(nameof(Confirm), new { token });

404チェックリスト(原因→対策を最短で潰す)

症状ありがちな原因確認ポイント対策
常に404actionName/controllerNameの指定ミスRedirectToActionの引数順・スペル・Controller名にControllerを付けていないかnameofの利用、引数順の見直し
開発はOKで本番だけ404ルーティング/Area/属性ルート差異Area指定の有無、MapControllerRouteの定義差areaルート値追加、ルート定義を統一
一見遷移しているが表示で404IDが取れずNotFoundしている遷移先のActionで再取得に失敗していないか、tokenの紐付け条件token保存のTTL/キー設計、ユーザー紐付けの確認
GETへ飛んだら404/405遷移先がPOST専用[HttpPost]しか無い、GET用Actionが無いGET用Actionを用意(PRGに合わせる)
ときどき404TempData/Session/キャッシュの期限切れ・複数タブ競合TTL、同時操作、Keep/Peekの挙動token方式+十分なTTL+再導線

404の切り分けのコツ:生成されたURLを目視する

RedirectToActionが生成したURLがそもそも期待と違う場合、ルーティングの問題です。ログやデバッグで遷移先URLを確認するか、開発時は一度だけ次のようにURLを生成して確認してみると原因が見つかりやすいです。

var url = Url.Action(nameof(Confirm), "Checkout", new { token });
// urlがnull/空、あるいは想定外ならルーティングの見直しポイント

設計で詰まりやすいポイント:複数タブ、戻るボタン、再読み込み

「ordersumをどこに置くか」は、実はユーザー操作の現実とも直結します。ECの購入フローは、次のような操作が普通に起こります。

  • 別タブでカートを開き直す
  • 戻るボタンで入力画面に戻って修正する
  • 確認画面を更新する
  • 決済途中でネットワークが切れて再試行する

TempDataやSessionに「固定キー」でordersumを入れていると、複数タブで上書きし、突然違う内容で確認画面が出る事故が起きやすくなります。そこでおすすめなのが、次の考え方です。

  • データ本体はサーバー側ストア
  • URLには推測困難なtoken(Guidなど)
  • tokenはユーザーに紐付けて検証(他人のtokenを弾く)
  • TTLを設け、期限切れ時は入力画面へ戻す導線を用意

この設計にすると、フローが長くなっても破綻しにくく、障害調査もしやすくなります。

よくある質問

Q. RedirectToActionでordersumを渡せた“ように見える”のに、なぜ非推奨?

A. たまたまプロパティが少なく、URLに収まっていたり、モデルバインドがうまく動いているだけの可能性が高いです。商品明細が増える・住所項目が増える・クーポンやポイント条件が増えるなど、実務ではすぐに肥大化します。さらに、URLへの露出やログ残りの問題が避けにくいため、将来の変更で壊れる前提の設計になりがちです。

Q. TempDataにordersumを入れて渡すのはどう?

A. 小さく短命なデータなら有効ですが、注文サマリーのように大きくなりがちなオブジェクトはおすすめしません。環境によってはCookieに入ってサイズ制限に当たる、セッションに入ってメモリを圧迫する、複数タブで衝突する、といった問題が起きやすいです。TempDataは「通知」や「一時フラグ」中心に使うのが安全です。

Q. どうしても保存(DB)したくない。最小の現実解は?

A. 分散キャッシュに短いTTLで保存し、URLはtokenのみ、がバランスが良いです。将来的にDBへ移したくなっても、インターフェース(IOrderDraftStore)を切っておけば差し替えやすく、コードの寿命が延びます。

Q. 404が出るけど、ルーティング?データ不足?

A. まず「生成されたURLが期待通りか」を確認し、次に「遷移先Action内でNotFoundしていないか」を確認します。前者ならRedirectToActionの引数順・controller名・Area/属性ルートが典型原因です。後者ならtokenの保存/取得、TTL、ユーザー紐付け、複数タブ競合が疑わしいです。

まとめ:判断基準は「Redirect=別リクエスト」を軸にする

RedirectToActionでモデル全体を渡したくなるのは自然ですが、Redirectは別リクエストであり、URLに載せられる範囲には限界があります。安定してスケールし、保守しやすい設計にするなら、次の判断基準が実務では強いです。

  • 注文サマリーのような重要データは、サーバー側に保存してIDだけ渡す
  • TempDataは「次のリクエストまでの小さな値」に留める
  • 404は「ルーティング問題」か「取得失敗NotFound」かを切り分ける
  • 複数タブ・戻る・更新に耐えるため、token方式+ユーザー紐付け+TTLを設計する

この記事を書いた人

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

コメント

コメントする

目次