ASP.NET MVC で Entity Framework を使ったアプリを触っていると、画面(.cshtml)はあるのに /Survey/Details/3 だけ 404(The resource cannot be found)になることがあります。多くの場合、原因は「ビューが無い」ではなく「ルーティングがアクションに届いていない」こと。この記事では切り分けから修正までを具体的に解説します。
現象のポイント:MVC は「ビュー」ではなく「アクション」に URL を割り当てる
まず大事な前提として、ASP.NET MVC では URL はビュー(cshtml)へ直接つながりません。URL は コントローラーのアクション(public メソッド)へルーティングされ、アクションが最終的に return View() などでビューを返します。
そのため、「cshtml があるのに 404」という状況は、MVC 的にはほぼ次のどちらかです。
- そもそも /Survey/Details/3 がアクションに到達していない(コントローラー/アクションが見つからない、ルートが合っていない)
- アクションには到達しているが、アクション側で 404 を返している(例:DB にレコードが無く
HttpNotFound()を返している)
つまり、最初にやるべきは「ビューを探す」ではなく、ルーティングとアクション解決の確認です。
まず切り分ける:IIS の 404 なのか、MVC の 404 なのか
同じ 404 でも、発生地点が違うと見当違いの調査をしがちです。特に「The resource cannot be found」という文言は、設定次第で IIS 側でも MVC 側でも出ます。そこで、“どこで 404 になっているか”を先に切り分けます。
| 見るポイント | MVC ルーティングで詰まっている可能性が高い | アクション到達後に 404 を返している可能性が高い |
|---|---|---|
| ブレークポイント | Details に置いても 止まらない | Details に置くと 止まる |
| 例外/ログ | 「アクションが見つからない」「コントローラーが見つからない」系 | HttpNotFound() を返したログ、DB 検索結果が null など |
| ブラウザ開発者ツール | レスポンス本文がフレームワーク標準の 404 画面(設定による) | アプリ独自の 404 表示、または同様の 404 でも処理時間が長いことがある |
結論として、今回のように「他の URL は動くのに、/Survey/Details/3 だけ 404」の場合、まずは Details アクションにブレークポイントを置いて止まるかどうかで大きく分岐させるのが最短です。
WebApiConfig は基本的に関係ない:見るべきは RouteConfig
質問でよくある混乱が WebApiConfig の設定です。api/{controller}/{id} のようなルートは ASP.NET Web API 用であり、通常の MVC(/{controller}/{action}/{id})とはルーティングの系統が別です。
/Survey/Details/3 のような URL が MVC の画面遷移なら、確認すべきは通常 App_Start/RouteConfig.cs(および Area を使っていれば AreaRegistration)です。
RouteConfig(MVC ルート)が想定どおりかを確認する
まず、既定のルートが以下のようになっているかを確認します。テンプレートから作った MVC アプリなら多くの場合この形です。
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 }
);
}
}
この既定ルートが有効なら、/Survey/Details/3 は次のように解釈されます。
| URL | controller | action | id | 到達先の想定 |
|---|---|---|---|---|
| /Survey/Details/3 | Survey | Details | 3 | SurveyController の Details(int id) など |
この前提で 404 になるなら、「SurveyController が見つからない」「Details アクションが見つからない」「アクション選択条件を満たしていない」のどれかが濃厚です。
ルートの “順番” にも注意する
MVC のルートは 上から順に評価されます。特定の URL パターンを別のルートで先に食ってしまうと、既定ルートまで到達しないことがあります。例えば、次のような「広くマッチするルート」が先頭にあると、想定外のアクションに流れる、または一致しない扱いになりやすいです。
// 例:先頭にあると影響が大きい(設計意図が明確な場合以外は注意)
routes.MapRoute(
name: "CatchAll",
url: "{*pathInfo}",
defaults: new { controller = "Error", action = "NotFound" }
);
専用ルートを追加する場合は、既定ルートより上に置くのが基本です(ただし“広すぎるルート”を上に置くと逆効果なので、パターンはできるだけ絞ります)。
本丸:SurveyController と Details アクションを “名前で” そろえる
/Survey/Details/3 が既定ルートで解釈されるなら、MVC は次を探しにいきます。
- SurveyController という名前のコントローラー(末尾が Controller)
- その中の public な Details アクション
最も多い原因は、コントローラーやアクションの名前が微妙に違うケースです。以下を機械的に確認してください。
| 確認項目 | OK の例 | NG になりやすい例 |
|---|---|---|
| コントローラー名 | public class SurveyController : Controller | SurveyControllers、SurveysController、SurveyControler(綴り違い) |
| アクション名 | public ActionResult Details(int id) | public ActionResult Detail(int id)(単数)、DetailsAsync だけ、private になっている |
| 戻り値 | ActionResult / ViewResult / Task<ActionResult> | 戻り値が void、または Controller と無関係な型 |
Detail(単数)と Details(複数)のズレは “404 の定番”
スキャフォールドや過去の実装で、アクション名が Detail(単数)になっているのに、リンクやルートが Details(複数)になっていると、/Survey/Details/3 は素直に 404 になります。
このズレの直し方は大きく 3 つあります。チームの URL 設計・既存のリンク数・外部公開している URL の有無で選びます。
| 方針 | やること | メリット | 注意点 |
|---|---|---|---|
| アクション名を Details に統一 | Detail を Details にリネーム | 最も素直で分かりやすい | 呼び出し側(リンク/リダイレクト)の修正が必要 |
| URL は Details、実装は Detail のまま | [ActionName("Details")] を付ける | コード上の名前を変えずに URL だけ合わせられる | 検索性が下がる(メソッド名と URL が一致しない) |
| RouteConfig で吸収 | 専用ルートを追加し Details を Detail に振り分ける | URL の後方互換を保ちやすい | ルートが増えすぎると管理が難しくなる |
例えば、実装が Detail のままでも /Survey/Details/3 を通したいなら、専用ルートを既定ルートより上に追加します。
routes.MapRoute(
name: "SurveyDetailsAlias",
url: "Survey/Details/{id}",
defaults: new { controller = "Survey", action = "Detail" },
constraints: new { id = @"\d+" }
);
// 既定ルートは最後に
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
制約(constraints)を入れておくと、意図しない文字列が入ったときに別ルートと競合しにくくなります。
“アクションがあるのに 404” になる属性(HttpGet/HttpPost/ChildActionOnly)を疑う
コントローラーにもアクションにも名前の問題がないのに「止まらない 404」が出るとき、見落としがちなのが アクション選択に影響する属性です。
HttpPost だけになっている
Details が POST 専用になっていると、ブラウザで /Survey/Details/3 を開く GET には一致せず 404 になります。
[HttpPost]
public ActionResult Details(int id)
{
// POST 専用のため、GET /Survey/Details/3 では呼ばれない
}
対策は、GET 用のアクションを用意するか、属性を見直します。
[HttpGet]
public ActionResult Details(int id)
{
// 画面表示は GET にするのが一般的
return View();
}
ChildActionOnly が付いている
[ChildActionOnly] が付いているアクションは、ビューからの @Html.Action() など “子アクション” としてのみ呼べます。URL 直叩きは 404 になります。
[ChildActionOnly]
public ActionResult Details(int id)
{
// URL から直接アクセスできない
}
もし「部分ビュー用のつもり」で付いているなら、画面用アクションと分ける(例:Details と _DetailsPartial)など、役割を分離するのが安全です。
ActionName / NonAction の影響
[ActionName("Details")] を付けると URL とメソッド名の対応が変わります。また、誤って [NonAction] が付いているとアクションとして認識されません。リファクタリング時に属性が残っていないか確認してください。
属性ルーティングを使っている場合:MapMvcAttributeRoutes の有無と Route の衝突
MVC5 では属性ルーティング([Route])が使えます。プロジェクトに routes.MapMvcAttributeRoutes() が入っている場合、RouteConfig の既定ルートだけでなく、各アクションの Route 属性もルート決定に参加します。
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
例えば、Details にだけ Route が付いていない/制約が厳しすぎると、想定と違う挙動になります。
[Route("Survey/Details/{id:int}")]
public ActionResult Details(int id) { ... }
{id:int} のような制約は安全ですが、もし {id:guid} にしていて URL が数値(3)なら一致しないため 404 になります。見た目が似ているので、制約の型は必ず確認してください。
Area を使っている場合:/Survey が “Area の Survey” なのかを確認する
アプリを再整備していると、画面単位で Area を導入していることがあります。Area を使うと、同じ controller 名でも別の名前空間/フォルダ配下に配置され、ルーティングも AreaRegistration 側で定義されます。
例えば Survey が Area 名で、URL が /Admin/Survey/Details/3 のようになる設計なのに、/Survey/Details/3 を叩いていると当然 404 になります。Area の route 定義(例:AdminAreaRegistration)と、想定 URL を合わせてください。
デバッグの実務手順:止まる/止まらないで調査ルートを分岐する
「どこをどうデバッグすればいいか分からない」を解消するために、現場で使える手順を整理します。ポイントは、最初の 5 分で“ルーティング問題か、処理中の 404 か”を決め打ちすることです。
| 手順 | やること | 分かること | 次に見る場所 |
|---|---|---|---|
| ブレークポイント | SurveyController の Details の先頭に置く | アクションに到達しているか | 止まらないなら RouteConfig / 属性 / Area |
| ActionLink の生成確認 | @Url.Action("Details","Survey", new { id=3 }) を出力 | アプリ側が生成する URL が正しいか | 生成結果が /Survey/Detail/3 なら名前ズレ |
| RouteData の確認 | 止まったら RouteData.Values を見る | controller/action/id がどう解釈されたか | 想定と違うならルート衝突 |
| 例外/ログ | Output/ログで 404 の原因メッセージを見る | アクション未発見か、HttpNotFound か | 原因に応じて修正 |
ブレークポイントが止まらない場合に見るチェックリスト
- SurveyController がプロジェクトに存在するか(参照先のアセンブリに移動していないか)
- クラス名が SurveyController になっているか(末尾 Controller、スペル)
- Details が public で定義されているか(private/protected になっていないか)
[HttpPost]や[ChildActionOnly]など、URL 直アクセスを拒否する属性が付いていないか- 属性ルーティングの制約(
{id:int}等)が、実際の URL と一致しているか - RouteConfig で “広すぎるルート” が先に評価されていないか
ブレークポイントは止まるのに 404 の場合:データが無くて HttpNotFound を返している
Details に到達しているのに 404 になる場合、最も多いのは「DB に対象データが無い」パターンです。スキャフォールドされた Details は、だいたい次のような形になっています。
public ActionResult Details(int id)
{
var survey = db.Surveys.Find(id);
if (survey == null)
{
return HttpNotFound(); // ここで 404
}
return View(survey);
}
この場合、/Survey/Details/3 だけ 404 になるのは、単純に ID=3 のレコードが存在しない可能性があります。再整備中にマスタデータが入っていない、ID が採番し直されている、テストデータの移行が未完了、といった状況では特に起こりがちです。
確認方法はシンプルです。
- SQL Server なら管理ツールで
SELECT * FROM Surveys WHERE Id = 3を実行 - EF ならデバッグ中に
db.Surveys.Any(x => x.Id == 3)をウォッチする - 画面一覧(Index)から Details リンクを辿り、そこで生成される id を確認する
もし「存在しない id を叩いても、ユーザーに分かりやすい画面を出したい」なら、404 のままカスタムビューを返す選択肢もあります(SEO 的には 404 を維持しつつ内容を整えるのが一般的です)。
public ActionResult Details(int id)
{
var survey = db.Surveys.Find(id);
if (survey == null)
{
Response.StatusCode = 404;
return View("NotFound"); // NotFound.cshtml を用意
}
return View(survey);
}
最短で直すための “型”:URL・ルート・アクションを 1 本に揃える
調査で原因が分かったら、修正は「ルート」「アクション名」「URL 生成」の三点を揃えるのが最も事故が少ないです。おすすめの落としどころは次の考え方です。
- 画面表示(Details)は GET にする
- アクション名は URL と一致させる(Details を Details に)
- URL は
@Url.Action/@Html.ActionLinkで生成し、手書きリンクを減らす
public class SurveyController : Controller
{
private readonly AppDbContext db = new AppDbContext();
[HttpGet]
public ActionResult Details(int id)
{
var survey = db.Surveys.Find(id);
if (survey == null) return HttpNotFound();
return View(survey);
}
}
そしてビュー側では、URL を直書きせずに生成します。
@Html.ActionLink("詳細", "Details", "Survey", new { id = item.Id }, null)
これで「Detail/Details のズレ」「id の付け忘れ」「ルート変更に伴うリンク切れ」をまとめて防げます。
再発防止:404 を “原因別” に見分けられるようにする
リファクタリング中は 404 が頻発しやすいので、開発環境では原因が追える設定にしておくと効率が上がります。
- 開発環境だけ
customErrorsを Off(または RemoteOnly)にして詳細を確認できるようにする - 404 を返す箇所(
HttpNotFound())にログを仕込む - ルーティングを増やすときは、専用ルートの条件を絞り「広すぎるルート」を作らない
特に「/Survey/Details/3 だけ 404」という症状は、名前のズレかデータ欠損で説明できることが多いです。ブレークポイントで到達可否を切り分け、RouteConfig と SurveyController(Details/Detail)を最優先で見直すのが、遠回りしない解決策になります。

コメント