ASP.NET Core MVC のテンプレートで、ビュー名を指定していないのに Views/Shared/Error.cshtml が表示される——これは設定ではなく「規約」による動作です。ビュー名決定の仕組みと Razor の探索順序、ログで確認する方法、探索パスのカスタマイズまで整理します。
起きている現象を言語化すると「Error という名前のビューが自動で選ばれている」
「指定していないのに Error.cshtml が出る」と感じる場面の多くは、次の条件がそろっています。
- コントローラーのアクションで
return View(model);のようにビュー名を省略している - そのアクション名が
Error(または MVC が最終的に “Error” をビュー名として扱う状況)になっている Views/Shared/Error.cshtmlが存在する
このとき MVC は「ビュー名を省略したなら、アクション名と同名のビューを探す」という規約で動きます。つまり、あなたが指定していないのは「ファイルパス」であって、フレームワークは“Error というビューを描画せよ”という意図を規約で補完している、という整理が最も理解しやすいです。
まず押さえるべき結論:ASP.NET Core MVC は「設定より規約」でビュー名を決める
MVC の Controller には View() のオーバーロードがいくつも用意されています。よく使うのはこのあたりです。
| 書き方 | ビュー名(ViewName) | 何が起きるか | 向いている場面 |
|---|---|---|---|
return View(); | 未指定(null) | 実行時に「アクション名」をビュー名として補完して探索 | 規約どおりに配置しているとき |
return View(model); | 未指定(null) | モデルだけ設定し、ビュー名は同様に「アクション名」で補完 | 典型的な MVC の書き方 |
return View("Error", model); | Error | ビュー名を明示し、Error を探す | アクション名変更・リファクタで壊したくないとき |
return View("~/Views/Shared/Error.cshtml"); | パス指定 | 探索規約を使わず、そのファイルを直接指定 | 特殊な配置や移行時の一時対応 |
ポイントは、View() の引数にビュー名が入っていないとき、MVC が「アクション名」を使ってビュー名を決めることです。ビュー名=ファイル名(拡張子なし)という感覚で捉えると、なぜ Error.cshtml に行き着くのかが見えます。
「そもそも Error アクションが呼ばれている」理由:テンプレートの例外ハンドリング
テンプレート(.NET 9 を含む近年の MVC テンプレート)では、開発環境と本番環境でエラーハンドリングの流れを変えるのが定番です。代表的な Program.cs は次のような形です。
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
// 例外が起きたら /Home/Error に「再実行」させる
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
UseExceptionHandler("/Home/Error") が入っていると、アプリ内で例外が未処理のまま上がってきたタイミングで、ミドルウェアがその例外を捕捉し、指定したパス(ここでは /Home/Error)を内部的に再実行します。あなたが明示的に Error() を呼んでいなくても、実行環境(特に本番相当)では「例外が起きたら Error アクションが呼ばれる」構成になっていることが多いわけです。
テンプレートにある HomeController の Error() は、だいたい次のようなコードになっています。
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
public class HomeController : Controller
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
var requestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
return View(new ErrorViewModel
{
RequestId = requestId
});
}
}
ここで View(...) にビュー名がありません。したがって MVC は「アクション名=Error」をビュー名として補完し、Error ビューを探しに行きます。
ビュー探索(View discovery / View location)の規約:Razor ビューエンジンが探す順序
では「Error というビュー」を具体的にどこで探すのでしょうか。答えは Razor ビューエンジンのビュー探索規約です。基本は次の二段構えです。
- まず
Views/{コントローラー名}/{ビュー名}.cshtml - 見つからなければ
Views/Shared/{ビュー名}.cshtml
今回の例で言うと、コントローラーが Home で、ビュー名が Error です。したがって候補は次の順になります。
| 探索順 | 候補パス | この例での結果 |
|---|---|---|
| 先 | Views/Home/Error.cshtml | 存在しない(テンプレートでは通常置かない) |
| 次 | Views/Shared/Error.cshtml | 存在するのでヒットして描画される |
つまり、「Shared フォルダも探索対象に含まれる」という規約があるため、ビュー名を省略しても Views/Shared/Error.cshtml が選ばれます。
(補足)Areas を使っている場合の探索順も押さえておく
業務アプリでは Areas を使うこともあります。Areas がある場合、探索パスが増えます。Razor ビューエンジンは概ね次のような候補を順に試します(代表例)。
| シナリオ | 候補パス例(ビュー名=Error) | 意図 |
|---|---|---|
| 通常(Area なし) | /Views/{controller}/Error.cshtml/Views/Shared/Error.cshtml | コントローラー単位 → 共通 |
| Area あり | /Areas/{area}/Views/{controller}/Error.cshtml/Areas/{area}/Views/Shared/Error.cshtml/Views/Shared/Error.cshtml | Area 内優先 → 全体共通へフォールバック |
この「フォールバックの階層」があるからこそ、テンプレートは Error のような“どこからでも使いたい画面”を Views/Shared に置きます。逆に言えば、コントローラー別にエラー画面を変えたいなら、Views/{Controller}/Error.cshtml を作るだけで Shared より優先されます。
ビュー探索はどこで決まっているのか:ViewLocationFormats とプレースホルダー
「規約」と言っても、完全にブラックボックスではありません。Razor ビューエンジンは RazorViewEngineOptions の設定(特に ViewLocationFormats)を使って探索候補を組み立てます。ここでよく出てくるのがプレースホルダーです。
| プレースホルダー | 意味 | Error 例(HomeController) |
|---|---|---|
{0} | ビュー名 | Error |
{1} | コントローラー名 | Home |
{2} | Area 名(なければ空) | (空) |
既定の探索フォーマット(代表例)は次のような形です。
/Views/{1}/{0}.cshtml
/Views/Shared/{0}.cshtml
# Area がある場合の代表例
/Areas/{2}/Views/{1}/{0}.cshtml
/Areas/{2}/Views/Shared/{0}.cshtml
このフォーマットに基づいて「実際の候補パス」を組み立て、上から順に存在チェックしていくのが “View discovery” の実体です。
「指定していないのに表示される」を確実に納得するための流れ図
ここまでの話を、発生順に並べると理解が固定されます。
| ステップ | 何が起きるか | 関係する要素 |
|---|---|---|
| 例外が発生 | アクションやミドルウェア内で未処理例外が起きる | アプリの実装 |
| 例外を捕捉 | UseExceptionHandler が例外を捕捉してエラーパスへ | app.UseExceptionHandler("/Home/Error") |
| アクション実行 | MVC ルーティングで HomeController.Error() が実行される | ルーティング(MapControllerRoute 等) |
| ViewResult 生成 | return View(model) によりビュー名未指定の ViewResult が返る | コントローラーの View() |
| ビュー名補完 | ビュー名が null のため、アクション名(Error)で補完される | “設定より規約” |
| ビュー探索 | Views/Home/Error.cshtml → Views/Shared/Error.cshtml の順に探索 | Razor の探索規約 |
| 描画 | Views/Shared/Error.cshtml が見つかり、HTML が返る | Razor View Engine |
この流れに「どこかに Error.cshtml を紐づける設定がある」と考える余地はほとんどありません。紐づけは 規約(アクション名とビュー名の一致+探索パスの順序)で成立しています。
実際にどこを探したのかを確認する方法:ログで “探索した場所” を見える化する
「規約で探しているのは分かった。でも、自分の環境で本当にその順で探している?」を確認したいときは、ログを活用すると一発です。Razor のログカテゴリを Debug にすると、ビュー探索の候補が出力されることがあります。
たとえば appsettings.Development.json に次のような設定を入れます。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore.Mvc.Razor": "Debug",
"Microsoft.AspNetCore.Mvc.ViewFeatures": "Debug"
}
}
}
すると、状況によっては次のようなログが確認できます(出力形式はバージョンや設定で変わります)。
... RazorViewEngine: The view 'Error' was found at '/Views/Shared/Error.cshtml'.
もしビューが見つからない場合は、次のように「探した場所の一覧」が出ることもあります。
... RazorViewEngine: View 'Error' was not found. The following locations were searched:
... /Views/Home/Error.cshtml
... /Views/Shared/Error.cshtml
ログが出ない場合でも、Visual Studio のデバッガで ViewResult の ViewName が null になっていること、実行時に “Error” として解決されることを追えば、同じ結論に到達できます。
「Shared を探す規約」があるメリット:エラー画面は共通部品として扱える
Views/Shared は、エラーページに限らず “全コントローラー共通で使うビュー” を置くためのフォルダです。代表的には次が該当します。
_Layout.cshtml(全体レイアウト)_ValidationScriptsPartial.cshtml(入力検証の共通パーツ)Error.cshtml(例外時の共通表示)
共通にしておくと、コントローラーが増えてもエラービューを増やす必要がなく、保守性が上がります。逆に「このコントローラーだけは別のエラー表示にしたい」となったときも、Views/{Controller}/Error.cshtml を作れば Shared より優先されるので、拡張も簡単です。
明示的にビューを指定したいときの実務的な選び方
規約のままでも問題ないケースが多い一方で、チーム開発や長期運用では “壊れにくさ” を優先してビュー名を明示したくなることもあります。実務で迷いがちな選択を表にまとめます。
| 目的 | おすすめ | 理由 |
|---|---|---|
| 規約どおりでシンプルに | return View(model); | アクション名と同名ビューで統一でき、MVC らしい |
| アクション名変更の影響を減らす | return View("Error", model); | [ActionName] やリネーム時にビュー探索が変わらない |
| 特定の場所のビューを強制 | return View("~/Views/Shared/Error.cshtml"); | 探索規約を迂回できるが、構造変更に弱いので限定的に |
| HTTP ステータス別に分けたい | 例外と 404 を分離して別アクションへ | 例外(500 系)と存在しない(404)では体験もログも違う |
「テンプレートは View 名を省略しているけど、チームでは明示する」という方針も普通にあり得ます。ルールは “動くかどうか” ではなく “将来の変更に強いか” で決めるのがコツです。
ビュー探索場所をカスタマイズしたい場合:RazorViewEngineOptions を使う
規約は固定ではありません。Razor の探索場所は RazorViewEngineOptions で追加・変更できます。たとえば「Views ではなく Features というフォルダにも置きたい」なら、探索パスを追加します。
using Microsoft.AspNetCore.Mvc.Razor;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.Configure<RazorViewEngineOptions>(options =>
{
// 先頭に入れると優先度が高くなる
options.ViewLocationFormats.Insert(0, "/Features/{1}/{0}.cshtml");
options.ViewLocationFormats.Insert(1, "/Features/Shared/{0}.cshtml");
});
これで、たとえば HomeController の Error を探すときに、次のような候補が増えます。
/Features/Home/Error.cshtml/Features/Shared/Error.cshtml/Views/Home/Error.cshtml/Views/Shared/Error.cshtml
注意点として、探索候補の順序(Insert/Add の順)はそのまま優先度になります。追加するなら「どれを優先したいか」を決めて配置するのが大事です。
より柔軟にしたいなら ViewLocationExpanders を使う
アプリの状態(テナント、テーマ、端末種別など)で探索場所を切り替えたい場合は、ViewLocationFormats の静的なリストだけでは足りません。そのときは IViewLocationExpander を使って、リクエストごとに探索候補を拡張できます。
using Microsoft.AspNetCore.Mvc.Razor;
public class ThemeViewLocationExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context)
{
// 例: クエリや Cookie からテーマを決める(ここでは固定)
context.Values["theme"] = "dark";
}
public IEnumerable<string> ExpandViewLocations(
ViewLocationExpanderContext context,
IEnumerable<string> viewLocations)
{
var theme = context.Values["theme"];
// 例: /Themes/{theme}/Views/... を優先する
var themed = new[]
{
$"/Themes/{theme}/Views/{{1}}/{{0}}.cshtml",
$"/Themes/{theme}/Views/Shared/{{0}}.cshtml"
};
return themed.Concat(viewLocations);
}
}
登録は次のように行います。
builder.Services.AddControllersWithViews()
.AddRazorOptions(options =>
{
options.ViewLocationExpanders.Add(new ThemeViewLocationExpander());
});
これを知っておくと、MVC を「巨大な Views フォルダ」にせず、機能単位やテナント単位で整理する設計が取りやすくなります。
ついでに理解が深まる:_ViewStart.cshtml と _ViewImports.cshtml も規約で探索される
ビュー探索の話をすると、Error.cshtml だけに目が行きがちですが、Razor にはもう一つ重要な規約があります。それが _ViewStart.cshtml と _ViewImports.cshtml の探索です。
| ファイル | 役割 | 探索のイメージ |
|---|---|---|
_ViewStart.cshtml | レイアウトの既定指定など、ビュー実行前の共通処理 | 現在のビューのフォルダから親方向へ辿って適用される |
_ViewImports.cshtml | @using や TagHelper の読み込みなど共通設定 | 同様に親方向へ辿って集約される |
「Shared に置く」「親方向へ辿る」といった規約が複数組み合わさることで、テンプレートは少ない記述で全体を成立させています。MVC の “設定より規約” を体感しやすいポイントなので、エラー画面だけでなく Razor 全体の仕組みとして理解しておくと応用が効きます。
エラー画面を実務品質にするためのチェックポイント
テンプレートの Error.cshtml は最小限です。実務では「表示」だけでなく「原因追跡」と「セキュリティ」を両立させる設計が重要になります。
開発環境と本番環境で表示を分ける
- 開発環境:開発向けの例外ページ(Developer Exception Page)で原因を追いやすくする
- 本番環境:
UseExceptionHandlerでユーザー向けの安全な画面に落とす
本番でスタックトレースを出すのは情報漏えいリスクがあります。テンプレートが本番で UseExceptionHandler を使うのは、単なるお作法ではなく安全策です。
RequestId(相関 ID)を画面とログでつなぐ
テンプレートの ErrorViewModel が RequestId を持っているのは、サポート問い合わせに強いからです。画面に RequestId を表示しておけば、ユーザーが「この番号のときに落ちました」と連絡し、運用側はログ検索で追跡できます。
例外(500)と 404 を分ける
「見つからない」は例外ではなく HTTP ステータスの話です。次のように分離すると、ログも UX も整理しやすくなります。
- 例外:
UseExceptionHandler("/Home/Error") - 404 等:
UseStatusCodePagesWithReExecute("/Home/StatusCode", "?code={0}")のように別アクションへ
MVC と Razor Pages の使い分け:Web Forms から来た人が混乱しやすいポイント
「ページ(aspx)を置けば表示される」感覚に近いのは Razor Pages です。一方 MVC は “アクション(C#)が起点でビューを選ぶ” ので、最初は「指定していないのに表示される」感覚になりがちです。違いを表で比較します。
| 観点 | MVC(Controllers + Views) | Razor Pages |
|---|---|---|
| 起点 | コントローラーのアクション | ページ(.cshtml) |
| URL とファイルの関係 | ルーティング設定・属性で柔軟に決まる | ページの配置が URL に直結しやすい |
| ビュー探索 | アクション名 → 既定パスを探索(Shared あり) | 基本はそのページが対象(探索というより“その場”) |
| 向いている規模 | 画面間で同じドメインを共有する中〜大規模に強い | ページ単位で閉じる小〜中規模に強い |
「Controller と View の分離で整理したい」なら MVC、「ページ中心でサクッと作りたい」なら Razor Pages、という考え方が失敗しにくいです。
よくあるハマりどころ Q&A
Error.cshtml を削除したらどうなる?
Views/Home/Error.cshtml も Views/Shared/Error.cshtml も無い状態で return View(); を実行すると、実行時に「ビューが見つからない」というエラーになります。つまり、Error.cshtml は “勝手に選ばれている” のではなく “見つかったから選ばれた” だけです。
Views/Home/Error.cshtml を追加したら、Shared は使われなくなる?
多くのケースでその通りです。通常は Views/{Controller}/{View}.cshtml が先に探索されるため、同名があればそちらが優先されます。コントローラー別のエラー画面を作りたいときは、この性質を利用するのが手堅いです。
アクション名を変えたら、勝手に違うビューを探し始めた
ビュー名省略の規約は “アクション名と同名” なので、アクション名を変えれば探すビュー名も変わります。[ActionName("Something")] を付けた場合も同様に影響することがあります。壊したくない場合は return View("Error", model); のように明示しましょう。
「Error」以外の名前で共通エラービューを置きたい
可能です。たとえば CommonError.cshtml を Shared に置いたなら、return View("CommonError"); と書けば OK です。MVC は “Error だから特別” なのではなく、単に “名前で探す” だけです。
まとめ:Error.cshtml が出るのは「規約で名前を補完し、規約で探して、見つかったから」
return View(model)でビュー名を省略すると、MVC はアクション名をビュー名として補完する- Razor ビューエンジンは
Views/{Controller}/{View}.cshtml→Views/Shared/{View}.cshtmlの順で探索する - テンプレートは共通エラー画面を Shared に置くため、
Views/Shared/Error.cshtmlがヒットしやすい - 探索順はログで確認でき、
RazorViewEngineOptionsでカスタマイズもできる
「どこかに紐づけ設定があるはず」と探すより、MVC の “設定より規約” と “ビュー探索規約” を一度腹落ちさせるのが最短です。理解できれば、Error だけでなく Index や Details、部分ビューやレイアウトにも同じ考え方を適用でき、設計の自由度が一気に上がります。

コメント