みなさんこんにちは!ヒロポンです!!
今回は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仲間いたら、どんどんシェア待ってるぜ!!

コメント