みなさんこんにちは!ヒロポンです!!
今回は ASP.NET 生存ガイド連載・第3回 の本論記事。WinForms 業務SE がガチで圧倒されるやつ!!の話。
ASP.NET MVC 5 のプロジェクトを開いて App_Start/RouteConfig.cs を覗いた瞬間に、「これ何?? Form 切替どこ??」「MapRoute ってメニュー項目みたいなもん??」って固まったこと、ないっすか??
俺も最初に流通系SIer時代に RouteConfig.cs 見た時は同じだった。**WinForms の Form 切替(メニュー → Form.Show())で全部書いてきた頭からすると、「URL ってメニューの代わり?」「ハードコードでいいんじゃ?」みたいな素朴な疑問が浮かぶ。3週間くらい混乱した末に「これ Form 切替の HTTP 版じゃん」**って気付いた瞬間に、ルーティングがいい感じに腹落ちしました。
連載第1回(Form ↔ Razor View 対応)で View、第2回(Controller = Form_Load 拡張版)で Controller を押さえたので、本記事は **「URL → Controller → Action の流れ」**を扱います。連載の「HTML レイヤー / ASP.NET レイヤー分離思考」が、第2回で「Controller / View / Service レイヤー分離」に進化したのと同じ構造で、第3回は 「URL レイヤー / Controller レイヤー / Action レイヤー」のリクエスト解決経路の3層分離 に再拡張します。
この記事では VS2019 / .NET Framework 4.7.2 / C# 7.3 / ASP.NET MVC 5 環境で、**ルーティングの基本(デフォルトルート / 属性ルート / ルート制約)**と、WinForms Form 切替 ↔ Routing の対応マップ、ミニマム検証を、コード6本でまとめます。後ろの「現場メモ」で、業務系チームでルール化した時の話も書いてる。
3行で結論:
- URL の解決は3層: ①ルートテーブル探索 → ②Controller 解決 → ③Action 解決(WinForms のメニュー → Form.Show() の HTTP 版)
- デフォルトルート1行で大半は解決(
{controller}/{action}/{id}パターン)、属性ルートは複雑な URL でだけ使う- **詰まったら「URL レイヤーか、Controller レイヤーか、Action レイヤーか」**で原因切り分け → 最小 Action でミニマム検証
著者の体験 — RouteConfig.cs を見て圧倒された話
正直に書いておきます。流通系SIer時代に初めて RouteConfig.cs を見た時、「これ全部書かないとダメなん??」って圧倒されたんですよね。WinForms の menuStrip1.Click → otherForm.Show() で動いてた頭からすると、URL ↔ Action の対応を1つずつ書く感覚が分からなかった。
転機は2つ:
- デフォルトルート1行(
{controller}/{action}/{id})で大半が解決すると気付いた瞬間 - 「URL レイヤー / Controller レイヤー / Action レイヤー」の3層で考えると原因切り分けが半分終わると分かった瞬間
例えば「/Customer/Detail/3 が動かない」時は:
- URL レイヤー → ルートテーブルで
Customer/Detail/3がマッチするか - Controller レイヤー → CustomerController クラスが存在するか
- Action レイヤー →
Detail(int id)Action が存在して引数 3 を受け取れるか
の3段階で見ると、原因がどこにあるか分かりやすい。**WinForms の Form 切替経験者にとって、ルーティングは「Form 切替の HTTP 版」**と言えるんですよね。
対応マップ — WinForms Form 切替 ↔ MVC Routing の8観点
WinForms と MVC の対応関係を、8観点でまとめたのがこんな感じ:
| WinForms 概念 | ASP.NET MVC 5 ルーティング | 違いのポイント |
|---|---|---|
| MainForm のメニュー項目 | ルートテーブル(RouteConfig.cs) | メニュー = 静的、ルート = URL パターンで動的 |
menuStrip1.Click → otherForm.Show() |
URL /Customer/Index → CustomerController.Index() |
UI イベント → HTTP リクエスト |
| Form の名前で識別 | コントローラ名 + アクション名で識別 | 物理的な Form インスタンス vs URL パターン |
| Form のコンストラクタ引数 | URL クエリ文字列 / ルート値 / Action 引数 | 値渡しの仕組みが違う |
this.Tag プロパティで状態渡し |
TempData / Session で状態渡し | サーバ側 HTTP リクエスト跨ぎ |
Form.Show() vs Form.ShowDialog() |
RedirectToAction vs PartialView |
UI ブロッキングの概念差 |
Form_Load → 初期化処理 |
Action メソッド本体 | 同期的に実行される起点 |
| MdiParent / MdiChild | Layout.cshtml + RenderBody | UI 構造の親子関係 |
ここから順に、コード対比で見ていきます。
定石1: デフォルトルート(Convention-based)の仕組み
ASP.NET MVC 5 のテンプレートが生成する App_Start/RouteConfig.cs のデフォルトルートはこんな感じ:
// ✅ 定石1: デフォルトルート({controller}/{action}/{id})
using System.Web.Mvc;
using System.Web.Routing;
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
ポイント:
{controller}/{action}/{id}パターンで URL の3階層を解析- デフォルト値: controller=Home / action=Index / id=オプション
/にアクセス → Home/Index/(id なし) に解釈される/Customer→ Customer/Index//Customer/Detail/3→ Customer/Detail/3
これだけで業務系の CRUD 5割は解決する。「/Customer/Index」「/Order/Edit/123」のような単純な URL は、デフォルトルート1行で吸収できるんですよね。
ん?じゃあ全部このルートで済むんじゃない??って思うかもだけど、業務系の現実では「URL を綺麗にしたい(/customers/123 のように REST 風)」「複雑な階層(/api/v1/orders/{orderId}/items/{itemId})」が出てくるので、その時に属性ルートを併用します。
定石2: 属性ルート(Attribute-based)— URL を綺麗にしたい時
Action に直接 [Route(...)] 属性を付けて URL を指定するパターン:
// ✅ 定石2: 属性ルート([Route] で Action に直接指定)
[RoutePrefix("customer")] // Controller 全体のプレフィックス
public class CustomerController : Controller
{
[Route("")] // /customer に対応
public ActionResult Index() { /* ... */ }
[Route("{id:int}")] // /customer/123 (id は int 制約)
public ActionResult Detail(int id) { /* ... */ }
[Route("search/{keyword}")] // /customer/search/abc
public ActionResult Search(string keyword) { /* ... */ }
[HttpPost]
[Route("create")] // POST /customer/create
public ActionResult Create(CustomerVm model) { /* ... */ }
}
属性ルートを使うには、RouteConfig.cs で routes.MapMvcAttributeRoutes(); を呼ぶ1行が要ります(デフォルトテンプレートには含まれてないので注意):
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// ↓ この1行で [Route(...)] 属性が有効になる
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
属性ルートのメリット:
- URL が Action のすぐ隣にあるので、URL ↔ Action の対応が読みやすい
- ルート制約(
{id:int}等)を URL の中に書ける - **REST 風 URL(
/api/v1/orders/{orderId}/items/{itemId})**が綺麗に書ける
業務系の判断軸: デフォルトルート寄せが基本、属性ルートは **「URL を綺麗にしたい」「REST 風 API」「複雑な階層」**の場面だけに絞ると、ルート定義が散らばらずに保守性が良くなります。
定石3: ルート制約({id:int} 等)で型違いを弾く
{id:int} のようなルート制約で、URL の型違いをルートテーブル段階で弾くパターン:
// ✅ 定石3: ルート制約で型違いを弾く
public class OrderController : Controller
{
[Route("order/{id:int}")] // id は int のみ受け付ける
public ActionResult Detail(int id) { /* ... */ }
[Route("order/code/{code:alpha:length(8)}")] // 英字8桁の code のみ
public ActionResult ByCode(string code) { /* ... */ }
[Route("order/{guid:guid}")] // GUID 形式のみ
public ActionResult ByGuid(Guid guid) { /* ... */ }
[Route("order/{date:datetime}")] // 日付形式のみ
public ActionResult ByDate(DateTime date) { /* ... */ }
}
組み込み制約の主なもの:
| 制約 | 意味 |
|---|---|
{id:int} |
整数 |
{id:long} |
長整数 |
{name:alpha} |
英字のみ |
{name:alphanumeric} |
英数字のみ |
{id:guid} |
GUID 形式 |
{date:datetime} |
日付形式 |
{value:length(8)} |
文字数指定 |
{value:minlength(3)} |
最小文字数 |
{value:range(1,100)} |
範囲指定 |
業務系で**{id:int} を付けないと**、/order/abc が来た時に Action 引数 int id の model binding が失敗して NullReferenceException、もしくは別のルートにフォールバックして意図しない Action が呼ばれる事故が起きる。ID 系パラメータは型制約をセットで書くのが業務系の鉄則っす。
定石4: カスタム階層ルート — REST 風 API のような綺麗な URL
業務系の API で /api/v1/orders/{orderId}/items/{itemId} のような階層 URL を扱うパターン:
// ✅ 定石4: カスタム階層ルート(API 風)
[RoutePrefix("api/v1")]
public class OrdersApiController : Controller
{
[Route("orders/{orderId:int}/items")]
public ActionResult ListItems(int orderId) { /* ... */ }
[Route("orders/{orderId:int}/items/{itemId:int}")]
public ActionResult GetItem(int orderId, int itemId) { /* ... */ }
[HttpPost]
[Route("orders/{orderId:int}/items")]
public ActionResult AddItem(int orderId, ItemVm model) { /* ... */ }
[HttpDelete]
[Route("orders/{orderId:int}/items/{itemId:int}")]
public ActionResult RemoveItem(int orderId, int itemId) { /* ... */ }
}
ポイント:
RoutePrefixで共通プレフィックスを Controller に付ける- HTTP メソッド属性(
[HttpPost][HttpDelete])と[Route]を併用 - URL の階層と Action 引数が自然にマッピングされる
業務系の社内 API・モバイルアプリ連携 API で頻出のパターンです。「API は REST 風、画面はデフォルトルート」のように使い分けると、ルート設計の見通しが良くなります。
定石5: Form 切替 ↔ RedirectToAction の対比
WinForms の Form 切替と Action のリダイレクト処理の対比:
// ✅ WinForms 版: Form を切り替える
public partial class CustomerListForm : Form
{
private void btnNewCustomer_Click(object sender, EventArgs e)
{
var form = new CustomerCreateForm();
form.Show();
this.Hide(); // 元の Form を隠す
}
}
// ✅ ASP.NET MVC 版: Action にリダイレクト
public class CustomerController : Controller
{
public ActionResult Index() { /* 一覧表示 */ }
public ActionResult Create()
{
return View(new CustomerVm()); // 入力フォーム表示
}
[HttpPost]
public ActionResult Create(CustomerVm model)
{
_service.Save(model);
TempData["Message"] = "登録しました";
return RedirectToAction("Index"); // ← 一覧画面にリダイレクト
// 引数: actionName, controllerName(省略時は同じ Controller)
}
}
RedirectToAction の引数の順序を間違えると意図しない URL に飛ぶので、業務系の罠の1つ。第1引数が actionName、第2引数が controllerName の順序を覚えておくのが鉄則っす。
定石6: TempData で状態をリクエスト跨ぎで渡す
WinForms の this.Tag プロパティでの状態渡しに相当する、TempData の使い方:
// ✅ 定石6: TempData でリクエスト跨ぎの状態渡し
[HttpPost]
public ActionResult Save(CustomerVm model)
{
_service.Save(model);
TempData["Message"] = "保存しました"; // 次のリクエストまで生存
TempData["LastSavedId"] = model.Id;
return RedirectToAction("Index");
}
public ActionResult Index()
{
if (TempData["Message"] != null)
{
ViewBag.Message = TempData["Message"]; // 1度読むと消える
}
if (TempData["LastSavedId"] is int savedId)
{
ViewBag.LastSavedId = savedId;
}
return View(_service.GetAll());
}
ポイント:
- TempData は次のリクエストまで生存(リダイレクト先で読める)
Sessionより短命(永続的な状態は Session、一時的なメッセージは TempData)- 読むと消える(
Peekで消さずに読むことも可能)
連載第2回で扱った PRG(Post-Redirect-Get)パターンの延長で、保存 → メッセージ表示 → 一覧画面に戻るフローの定石パターンっす。
ハマりポイント — 実体験ベースの本番事故3点
1. ルート優先順位の理解不足で予期せぬ Action が呼ばれた(半日デバッガで追ってハマった)
属性ルートとデフォルトルートを混在させた時、優先順位を理解せずに意図しない Action が呼ばれて画面が崩れた事件。夕方の運用報告で気づいて半日デバッガで追ってハマった末に、**「属性ルート > Convention-based、具体 > 抽象」**の優先順位ルールを学んだ。それ以来、業務系チームで 「属性ルートを使う時は Controller 単位で全 Action に付ける(混在しない)」 をルール化した。
2. {id:int} 制約なしで customer/abc が int 期待 Action に到達して例外(30分溶かした)
/customer/{id} ルートで id を int 期待していたのに、URL に文字列が来て NullReferenceException で詰まった事件。30分溶かした末に、{id:int} のルート制約を入れてルートテーブル段階で弾く形に直した。それ以来、ID 系パラメータは型制約をセットで書くを業務系チーム規約に揃えました。
3. RedirectToAction の引数間違いで別 Controller に飛んだ(夕方の運用報告で気づいた)
RedirectToAction("Customer", "Edit") と書くべきところを RedirectToAction("Edit", "Customer") と引数順を間違えて、Edit Controller の Customer Action を探しに行って 404 になる事件。夕方の運用報告で気づいた。RedirectToAction(actionName, controllerName) の引数順を覚えるか、RedirectToAction("Index", new { controller = "Customer" }) のように名前付きで書くと事故りにくい。
著者の現場メモ — 業務系チームでのルーティング規約
流通系SIer時代に過去の RouteConfig.cs と Controller を grep -rnE "MapRoute|\[Route\(|RedirectToAction" . で70箇所近くひっかけたら、属性ルートとデフォルトルートが混在・{id} 制約なし・RedirectToAction 引数順ミスが全部入りだった。後輩と一緒に 3行ルール にまとめた:
- デフォルトルート寄せ、属性ルートは API・複雑階層の Controller のみ(混在禁止)
- ID 系パラメータは型制約をセットで書く(
{id:int}{id:guid}等) RedirectToActionは(actionName, controllerName)の順を死守 or 名前付き引数で書く
このルール化で、ルーティング周りの 404 / 例外 / 意図しない Action 起動が消えた。書き方を1パターンに揃えるだけで保守工数と事故率が両方下がるので、業務系チームにはおすすめのルールっす。
まとめ
| 状況 | WinForms の対応 → ルーティングの答え |
|---|---|
| メニューから画面遷移 | MapRoute でルート定義 / Action にデフォルトルートで到達 |
| シンプルな CRUD | デフォルトルート1行 {controller}/{action}/{id} |
| URL を綺麗にしたい・REST 風 | 属性ルート [Route("api/v1/...")] |
| 型違いを弾きたい | ルート制約 {id:int} {id:guid} 等 |
| Form を切り替える | RedirectToAction(actionName, controllerName) |
| 状態を次画面に渡す | TempData["Key"] = value; |
| 大規模システムの分離 | Areas(Admin/API/Public) |
| ルートが効かない時 | 最小 Action でミニマム検証 → 段階的に複雑化 |
WinForms 業務SE が ASP.NET ルーティングに移行する時の事故は、「URL/Controller/Action の3層分離思考」「デフォルトルート寄せ」「型制約セット」 の3点で9割消えます。次回(連載第4回)は「ORM 3択 — EF6 / Dapper / ADO.NET の業務SE 視点比較」を扱うので、Action 内部での DB アクセス方針が見えてきます。
よくある質問
Q1. デフォルトルートと属性ルートはどう使い分けますか?
A. 原則は デフォルトルート(Convention-based)でほぼ全部解決、属性ルートは「URL を綺麗にしたい・REST 風 API・複雑な階層」の場面だけに絞るのが業務系の判断軸です。/Customer/Index/3 のような単純な CRUD はデフォルトルート1行で十分で、/api/v1/orders/{orderId}/items/{itemId} のような階層がある時だけ [Route(...)] 属性ルートを Action に直接付ける。最初は混在させず、デフォルトルート寄せから始めるのが学習コストを下げます。
Q2. ルート制約 {id:int} を付けないと何が起きますか?
A. 型違いの値が来た時に Action パラメータの model binding で例外、もしくは想定外の Action にマッチします。例えば /Customer/Detail/{id} で id が int 期待なのに /Customer/Detail/abc が来ると、id が null になって NullReferenceException が飛んだり、別のルートにフォールバックして意図しない Action が呼ばれたりする。{id:int} {id:guid} {name:alpha} のような組み込み制約を入れておくと、ルートテーブルの段階で弾けるので安全です。
Q3. RedirectToAction の引数を間違えると何が起きますか?
A. 意図しない Action にリダイレクトされます。RedirectToAction("Edit", "Customer") を RedirectToAction("Customer", "Edit") と書くと、Customer Controller の Edit Action ではなく、Edit Controller の Customer Action を探しに行って 404 か別の Action が呼ばれる。第1引数が actionName、第2引数が controllerName の順序を覚えておくと事故りにくいです。
Q4. Areas はいつ使うべき?
A. Admin / API / Public のような大規模な画面群を分離したい時に使います。中小規模の業務系画面1〜2画面なら Areas は不要で、Controllers/ フォルダにフラットに並べる方が見通しが良い。Admin 画面が10個以上ある・API と Web 画面を同居させたい・別のレイアウトを当てたい、のような条件が揃った時に Areas 化するのが業務系の判断軸です。
Q5. ルートが効かない時の最短デバッグ手順は?
A. ミニマム検証で行きます。(1)Hello を返すだけの最小 Action を作って /Hello/Index で呼ぶ → ここで動かない時はルート設定 or web.config 問題、(2)動いたら本番のルートを少しずつ近づける、(3)属性ルートを付けてみる、の段階で原因切り分け。最初に複雑な属性ルートで検証すると問題箇所が分散するので、デフォルトルート 1個から始めるのが業務系の鉄則です。
ここまでで ASP.NET ルーティングの基本・属性ルート・制約・Areas は押さえた。次回は ORM 3択を扱うので、Action 内部の DB アクセス方針が見えてきます。隣接トピックも貼っておきます。
関連記事
- WinForms の Form と Razor View の対応関係を業務SE が一日で腹落ちさせる — 連載第1回・View 側のマッピングを先に押さえたい時に効く
- C# WinForms の Form.ShowDialog と Form.Show の違いと使い分け完全ガイド — Form 切替の WinForms 知識を整理する時に効く
- C# のコールバック・デリゲート・イベントの違いを業務SEが30分で腹落ちさせる — URL 解決とイベント発火の概念対応を整理する時に効く
ASP.NET 生存ガイド・連載目次
本記事は WinForms 業務SE のための ASP.NET 生存ガイド 全10回の 第3回 です。
- 第1回: WinForms の Form と Razor View の対応関係を業務SE が一日で腹落ちさせる
- 第2回: Controller は WinForms の Form_Load 拡張版だと理解する — ASP.NET MVC 5 業務SE 入門
- 第3回(本記事): ルーティング — WinForms の Form 切替との対応 ← イマココ
- 第4回: ORM 3択 — EF6 / Dapper / ADO.NET の業務SE 視点比較(公開予定)
- 第5回: DI は業務系で必要か — 入れない派の論点(公開予定)
- 第6回: 業務イントラの認証 — Windows認証 / Forms認証 / Cookie(公開予定)
- 第7回: CSS が効かない時のチェックリスト10項目(公開予定)
- 第8回: IIS デプロイ — オンプレ業務系の現実(公開予定)
- 第9回: ASP.NET MVC 5 トラブルシューティング・チェックリスト20項目(公開予定)
- 目次(最終回): WinForms 業務SE のための ASP.NET 生存ガイド・全体目次(公開予定)
以上!
同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!

コメント