Skip to content

MS_ASPNETMVCModernization

nishi_74322014 edited this page Aug 21, 2026 · 1 revision

ASP.NET MVC の Modernization

概要

コチラの内容をまとめた。

補足(本ページの位置付け): 本ページは
Open 棟梁のテンプレートを近代化した際の実作業記録である。
対象は .NET Framework 上の ASP.NET MVC 5

【近代化の内容(当時)】
   ① OWIN 化                 → [OWIN化手順]
   ② Bootstrap / jQuery のリンク
   ③ Bundle & Minification、CDN フォールバック
   ④ マスタ ページ(レイアウト)の整備
   ⑤ Model Binding、非同期コントローラーの採用

①・⑤ は現在も有効な方向である(Core への布石)。
②・③ はフロントエンドの潮流が変わったため、
現在は別の手段を採る(各節で補足する)。

共通項

ASP.NET の Modernization を参照。

基本的な事項は、ASP.NET の BundleConfig を参照。

Bundle & Minification

必要に応じて、追加の BundleConfig を行う。

インストール

共通項でインストールしたパッケージに加え以下をインストールする。

  • jQuery Validation Plugin ( MVC の Validation で使っている )

    • 既存のフォームのドロップイン検証を提供する。
      アプリケーションに合わせてあらゆるカスタマイズを簡単できる。

    • インストール方法

      Install-Package jQuery.Validation
  • jQuery Unobtrusive Ajax
    https://github.com/aspnet/jquery-ajax-unobtrusive

    • 公式サイトにも殆ど情報が無いが、Ajax.BeginForm を実行するのに必要らしい。

    • インストール方法

      Install-Package Microsoft.jQuery.Unobtrusive.Ajax
  • jQuery Unobtrusive Validation
    https://github.com/aspnet/jquery-validation-unobtrusive

    • 公式サイトにも殆ど情報が無いが、Ajax.BeginForm での Validation を実行するのに必要らしい。

    • インストール方法

      Install-Package Microsoft.jQuery.Unobtrusive.Validation

補足(Unobtrusive の意味と仕組み): 「公式サイトにも殆ど情報が無い」
という原文の観察は正しく、当時から資料が乏しかった
仕組みを補っておく。

**Unobtrusive(控えめ、非侵入的)**とは、
HTML に JavaScript を直接書かないという設計方針を指す。

<!-- ✗ Obtrusive(HTML に JS が混ざる) -->
<input onblur="if(this.value=='') alert('必須です');">

<!-- ○ Unobtrusive(data-* 属性でメタ情報だけ持たせる) -->
<input name="Name"
       data-val="true"
       data-val-required="名前は必須です"
       data-val-length-max="50">
<!-- ↑ JS 側がこの属性を読んで検証を組み立てる -->
【動作の流れ(Validation)】

 ① ViewModel にデータ注釈を付ける
      [Required(ErrorMessage="名前は必須です")]
      public string Name { get; set; }
           ↓
 ② Html.TextBoxFor(m => m.Name) が data-val-* 属性を出力
           ↓
 ③ jquery.validate.unobtrusive.js が data-val-* を読む
           ↓
 ④ jquery.validate.js に検証ルールとして登録
           ↓
 ⑤ 【クライアント側で検証】される(サーバ側でも当然検証される)

依存関係の順序が重要である。

jquery.js
  └ jquery.validate.js
       └ jquery.validate.unobtrusive.js   ← この順に読む

  → 順序を間違えると【何も起きない】(エラーも出ない)
  → [ASP.NET の BundleConfig] で「順序を明示せよ」と述べた理由

Ajax.BeginForm も同じ仕組みである。

<!-- Ajax.BeginForm が出力する属性 -->
<form data-ajax="true"
      data-ajax-method="POST"
      data-ajax-update="#result"
      data-ajax-mode="replace">
<!-- ↑ jquery.unobtrusive-ajax.js がこれを読んで Ajax 送信に変える -->

移行メモ(現在の状況):

ライブラリ 現況
jQuery Validation 現役。ASP.NET Core でも使われる
jquery-validation-unobtrusive 現役Microsoft.jQuery.Unobtrusive.Validation)。Core のテンプレートにも入る
jquery-ajax-unobtrusive ASP.NET Core のテンプレートからは外れた
Ajax.BeginForm ASP.NET Core には存在しない

Ajax.BeginForm の現在の代替:

・ASP.NET Core MVC   … fetch API を自分で書く
・Razor Pages         … 同上
・Blazor              … コンポーネントのイベントで完結
・htmx                … 属性駆動という点で Unobtrusive Ajax の思想に近い ★
<!-- htmx(Unobtrusive Ajax の現代版と言える) -->
<form hx-post="/api/save" hx-target="#result" hx-swap="innerHTML">

「HTML に属性を書くと JS が動く」という発想自体は
現在も生きている
——という点で、この節の内容は
技術の変遷を追ううえで示唆的である。

定義方法

  • BundleConfig を追加する。
public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        // see : https://www.asp.net/ajax/cdn

        string jqueryVersion = "3.1.1";

        BundleTable.EnableOptimizations = true;
        BundleTable.Bundles.UseCdn = true; // same as: bundles.UseCdn = true;

        // ( new ScriptBundle("~/XXXX") のパスは実在するpathと被るとRender時にバグる。
        // なので、bundlesと実在しないpathを指定している(CSSも同じbundlesを使用する)。

        bundles.Add(new ScriptBundle("~/bundles/app").Include(
                    "~/Scripts/app/Site.js"));

        bundles.Add(new ScriptBundle("~/bundles/otr").Include(
                    "~/Scripts/otr/common.js",
                    "~/Scripts/otr/ie_key_event.js",
                    "~/Scripts/otr/else.js"));

        bundles.Add(new ScriptBundle(
            "~/bundles/jquery",
            string.Format("//ajax.aspnetcdn.com/ajax/jquery/jquery-{0}.min.js", jqueryVersion))
            {
                CdnFallbackExpression = "window.jQuery"
            }.Include(string.Format("~/Scripts/jquery-{0}.js", jqueryVersion)));

        bundles.Add(new ScriptBundle(
            "~/bundles/jqueryval",
            "//ajax.aspnetcdn.com/ajax/jquery.validate/1.16.0/jquery.validate.min.js")
            {
                CdnFallbackExpression = "window.jQuery.validator"
            }.Include("~/Scripts/jquery.validate.js"));

        bundles.Add(new ScriptBundle(
            "~/bundles/jqueryvaluno",
            "//ajax.aspnetcdn.com/ajax/mvc/5.2.3/jquery.validate.unobtrusive.min.js")
            {
                CdnFallbackExpression = "window.jQuery.validator.unobtrusive"
            }.Include("~/Scripts/jquery.validate.unobtrusive.js"));

        bundles.Add(new ScriptBundle("~/bundles/jqueryunoajax").Include(
            "~/Scripts/jquery.unobtrusive-ajax.js")); // CDNで提供されていない。
        
        // 開発と学習には、Modernizr の開発バージョンを使用します。次に、実稼働の準備ができたら、
        // http://modernizr.com にあるビルド ツールを使用して、必要なテストのみを選択します。
        bundles.Add(new ScriptBundle(
            "~/bundles/modernizr",
            "//ajax.aspnetcdn.com/ajax/modernizr/modernizr-2.8.3.js") // min 無し
            {
                CdnFallbackExpression = "window.Modernizr"
            }.Include("~/Scripts/modernizr-*"));

        bundles.Add(new ScriptBundle(
            "~/bundles/bootstrap",
            "//ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js")
            {
                CdnFallbackExpression = "window.jQuery.fn.modal"
            }.Include("~/Scripts/bootstrap.js"));

        bundles.Add(new ScriptBundle(
            "~/bundles/respond",
            "//ajax.aspnetcdn.com/ajax/respond/1.4.2/respond.min.js")
            {
                CdnFallbackExpression = "window.respond"
            }.Include("~/Scripts/respond.js"));

        bundles.Add(new StyleBundle("~/bundles/css").Include(
                    "~/Content/bootstrap.css",
                    "~/Content/themes/base/all.css",
                    "~/Content/themes/base/jquery-ui.css",
                    "~/Content/otr/Style.css",
                    "~/Content/app/Site.css"));
    }
}

補足(このコードの読みどころ): 実務的な工夫が詰まっているので、
要点を拾っておく

CdnFallbackExpression の値の選び方

ライブラリごとに「読み込めていれば定義されるグローバル変数」を指定する

   jQuery                    → window.jQuery
   jQuery Validate           → window.jQuery.validator
   jQuery Validate Unobtrusive → window.jQuery.validator.unobtrusive
   Bootstrap(JS)            → window.jQuery.fn.modal   ★ 巧い
   Modernizr                  → window.Modernizr
   Respond.js                 → window.respond

Bootstrap の window.jQuery.fn.modal が特に巧みで、
Bootstrap は独自のグローバル変数を作らないため、
jQuery プラグインとして登録される関数の存在で判定している。

// CDN で提供されていない。 というコメント

jquery.unobtrusive-ajax.js だけ CDN 指定がない。
実際に aspnetcdn では配信されていなかったためで、
調べた結果を残している点が有用である。

modernizr-* というワイルドカード

.Include("~/Scripts/modernizr-*")
  → バージョン番号を書かずに済む
  → ただし【複数マッチすると順序が不定】になる(1 つだけ置くこと)

EnableOptimizations = true を明示している

debug="true" でも最適化を有効にする、という意思表示である
ASP.NET の BundleConfig
「本番相当で検証せよ」に対応する)。

移行メモ(このコードに含まれるライブラリの現況):

ライブラリ 現況
jQuery 3.1.1 3.x 系は現役だが、3.1.1 には既知の脆弱性(XSS)。3.5 以上へ
Bootstrap 3.3.7 サポート終了5.x へ(4 で jQuery 依存が減り、5 で不要に
Modernizr 2.8.3 現在はほぼ不要(対象ブラウザが機能を備えているため)
Respond.js 不要(IE8 の Media Query 対応用。IE 自体が終了)
ajax.aspnetcdn.com 提供終了(Microsoft Ajax CDN は 2022 年に廃止)★

ajax.aspnetcdn.com の廃止は重要である。
このコードをそのまま動かすと、CDN が 404 になり、
フォールバックが毎回発動する
(動くが遅い)。
既存アプリを保守しているなら、CDN 指定を外すか、
別の CDN(jsDelivr、cdnjs)に差し替える必要がある。

ModernizrRespond.js は削除して構わない
IE のサポートが終了した現在、存在意義がない

  • BundleConfig を呼び出す。
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        // アプリケーションの設定方法の詳細については、http://go.microsoft.com/fwlink/?LinkID=316888 を参照してください

        // アプリケーションのスタートアップで実行するコードです

        //
        AreaRegistration.RegisterAllAreas();

        // 
        WebApiConfig.Register(GlobalConfiguration.Configuration);

        // グローバルフィルタの登録
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

        // URLルーティングの登録
        RouteConfig.RegisterRoutes(RouteTable.Routes);

        // バンドル&ミニフィケーションの登録
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
}

補足(Startup へ寄せた形になっている): OWIN化手順
「Global.asax から移動しても良いが必要性はない」という記述に対し、
ここでは実際に Startup へ寄せている

【この判断の利点】
   起動時の処理が 1 箇所に集まる
     → ASP.NET Core の Program.cs / Startup.cs に近い形になる ★
     → 移行時に対応関係が見えやすい

ASP.NET Core での対応形:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add<MyGlobalFilter>();   // ← FilterConfig 相当
});

var app = builder.Build();

app.UseStaticFiles();                         // ← BundleConfig 相当は消滅
app.UseRouting();
app.MapControllerRoute(                       // ← RouteConfig 相当
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapControllers();                         // ← WebApiConfig 相当

app.Run();

AreaRegistration.RegisterAllAreas() に相当するものは Core にはない
(エリアは属性 [Area("Admin")] とルート パターンで表現する)。

CDNフォールバック

  • ASP.NET MVC の CDN フォールバックは、
    ScriptBundle クラスで行なう。
  • ASP.NET Web Forms では、コレに加えて、
    ScriptManager に CDN フォールバック機能がある。
  • ASP.NET MVC の CDN フォールバックの設定方法など、
    詳しくは、コチラを参照。

リンクのさせ方

@* マスタ・ページに相当 *@
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>@ViewBag.Title</title>
    @Styles.Render("~/bundles/css")
    @Scripts.Render("~/bundles/modernizr")
    @Scripts.Render("~/bundles/jquery")

    @RenderSection("HeaderSection", required: true)

</head>
<body onload="Fx_Document_OnLoad(); Fx_AdjustStyle();">

    @RenderBody()

    @Scripts.Render("~/bundles/jqueryval")
    @Scripts.Render("~/bundles/jqueryuno")
    @Scripts.Render("~/bundles/otr")
    @Scripts.Render("~/bundles/app")

    @RenderSection("FooterSection", required: true)
</body>
</html>

移行メモ(バンドル名の不一致): レイアウトが参照している
~/bundles/jqueryuno は、
BundleConfig 側の定義(~/bundles/jqueryvaluno /
~/bundles/jqueryunoajax)と一致していない

【BundleConfig で定義されているもの】
   ~/bundles/jqueryval        (jquery.validate)
   ~/bundles/jqueryvaluno     (jquery.validate.unobtrusive)
   ~/bundles/jqueryunoajax    (jquery.unobtrusive-ajax)

【レイアウトが参照しているもの】
   ~/bundles/jqueryval        ○
   ~/bundles/jqueryuno        ✗ ← 定義されていない

Scripts.Render は、存在しないバンドル名を渡されると
それを「仮想パス」として <script src="…"> を出力する

(例外にはならない)。
結果、404 になる <script> タグが出力される

意図としては ~/bundles/jqueryvaluno と読める
(Validation の Unobtrusive を本文末尾で読み込む構成のため)。

この種の誤りは実行しても気付きにくい
(検証が効かなくなるだけで、エラーは出ない)。
ブラウザの開発者ツールのネットワーク タブで 404 を確認する
のが確実である。

補足(レイアウトの現在的な改善点): 移行元のマークアップには、
現在なら直すべき点がいくつかある。

<!-- ✗ 原文 -->
<html>
<body onload="Fx_Document_OnLoad(); Fx_AdjustStyle();">
<!-- ○ 現在 -->
<html lang="ja">        <!-- ① 言語を明示(読み上げ・フォント選択・SEO) -->
<body>
  ...
  <script>
    // ② onload 属性ではなく、スクリプト側で登録する(Unobtrusive)
    document.addEventListener('DOMContentLoaded', () => {
        Fx_Document_OnLoad(); Fx_AdjustStyle();
    });
  </script>
箇所 改善
<html> lang="ja" を付ける(国際化対応項目
<body onload> 属性に JS を書かない。CSP を導入すると動かなくなる ★
<meta charset> <head> の先頭に置くエンコーディング
viewport <meta name="viewport"> がない(レスポンシブに必要)
required: true セクション未定義のビューでエラーになる。required: false の方が扱いやすい

onload 属性は CSP(Content Security Policy)と衝突する
script-src 'self' のような CSP を設定すると、
インライン ハンドラは実行されない
セキュリティ強化を進めるなら、属性の JS は必ず外に出す

ASP.NET MVC では、既定で設定ができている。

その他

Model Binding や非同期コントローラーを使用するように修正する。

補足(この 2 点が最も本質的な近代化): Bundle や CDN は
潮流とともに古びたが、Model Binding と非同期化は現在も正しい方向である。

① Model Binding

// ✗ 古い書き方(Request から直接取る)
public ActionResult Save()
{
    var name = Request.Form["Name"];
    var price = decimal.Parse(Request.Form["Price"]);  // 例外の危険
    ...
}

// ○ Model Binding
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Save(ProductEditViewModel model)
{
    if (!ModelState.IsValid) return View(model);   // 検証も自動
    ...
}
【得られるもの】
   ・型変換とその失敗の扱いが自動
   ・データ注釈による検証(クライアント・サーバ両方)
   ・単体テストが書ける(引数を渡すだけ)
   ・[ASP.NET MVCでDataTableを使用する。] で述べた
     「双方向バインディングには POCO が要る」と同じ話

② 非同期コントローラー

// ✗ 同期(DB / API を待つ間、スレッドを占有する)
public ActionResult Index()
{
    var data = _repository.GetAll();
    return View(data);
}

// ○ 非同期
public async Task<ActionResult> Index()
{
    var data = await _repository.GetAllAsync();
    return View(data);
}
【効果】
   I/O 待ちの間、【スレッドがスレッド プールに返る】
     → 同時接続数に対してスレッドが足りる
     → スループットが上がる(レイテンシは改善しない)★

【注意】
   ・.Result / .Wait() を混ぜると【デッドロック】する
   ・「全部 async にすれば速くなる」わけではない
     → CPU バウンドな処理には効果がない

詳細は async/await を参照。

ASP.NET Core では非同期が既定であり、
同期版の API がむしろ削られているStream の同期読み書きが
既定で禁止される等)。
この方向への修正は、Core 移行の準備としても有効である。

参考

Bundle と Minification

CDNフォールバック

ASP.NET の Modernization を参照)

・・・

Microsoft Learn


Tags: 移行, .NET開発, ASP.NET, ASP.NET MVC, OWIN, NuGet

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally