ASP.NET Core MVCでError.cshtmlが勝手に表示される理由|Views/Sharedとビュー探索規約

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、部分ビューやレイアウトにも同じ考え方を適用でき、設計の自由度が一気に上がります。

この記事を書いた人

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

コメント

コメントする

目次