ASP.NET Web FormsからRazor Pages/.NET 8へ段階移行する実践ガイド|Upgrade Assistant・ViewState廃止・ユーザーコントロール再実装まで

十数年育ててきた ASP.NET Web Forms(.aspx)資産を、止めずに・崩さずに・速く .NET 8 へ移す――本記事はそのための現実解を、具体的な対応表・コード例・工程設計・失敗しがちな落とし穴まで含めてひとつにまとめた実践ガイドです。段階移行でリスクを可視化し、運用しながら置き換える道筋を示します。

目次

移行の結論(まず要点)

下記は、.NET Framework 4.5/4.8 の Web Forms から Razor Pages/.NET 8 へ移行するときに最初に意思決定すべきポイントの要約です。

項目解決策・ポイント効果/期待値
移行支援ツールMicrosoft 公式 .NET Upgrade Assistant を先に実行し、
プロジェクトを SDK スタイルへ変換・参照の互換化を半自動化。
UI 生成は MVC 雛形まで。Razor Pages の画面は手実装。
基盤のビルド可まで短縮。破壊的変更の洗い出しが早まる。
段階移行Upgrade Assistant のプロキシ ブリッジで旧サイトを /legacy に、
新サイトを /new として共存。ページ単位で差し替え。
無停止移行に近づく。影響範囲を限定して検証できる。
UI 書き換え.ascx は ViewComponent か Tag Helper に再実装。
ポストバック中心の設計は廃止し、モデルバインディング+REST に転換。
ViewState 依存解消。テスト容易性・SSR/CSR 両立。
代替技術イベント駆動 UX を維持したいなら Blazor を検討。
旧ユーザーコントロールの構造を Blazor コンポーネントへ移植しやすい。
低コストで Web Forms 的な開発体験を継承可能。
コードビハインドRazor Pages の PageModel(OnGet/OnPost)へ移植。
VB.NET は C# へ全面書換えが必須。
責務分離と依存性注入が進み、保守性が向上。
データアクセス既存 ADO.NET/ストアドは .NET 8 でも動作可。
移行を機に EF Core または Dapper へ置換すると良い。
クエリのテスト・差分吸収が容易。可読性と性能の両立。
推奨手順①ロジック/UI 分離 → ②Upgrade Assistant → ③共通部品の VC/TH 化 → ④ページ単位 Razor 化 → ⑤自動テスト → ⑥UX 改善/削減。小さく早く勝つサイクルを運用下で回せる。
注意点Razor Pages は ViewState を持たない。状態は TempData、
Hidden、Cookie、キャッシュで管理。Web Forms 特有 API は設計し直し。
パフォーマンス/再現性が安定。状態破損を回避。

Tips:最初の目標は「ビルドが通る」。アクセス頻度の低い領域から置換し、共通レイアウト(_Layout.cshtml)を先に固めると歩留まりが上がります。新規開発は必ず新側に載せる方針で横展開の手戻りを断ち切ります。

なぜいま Web Forms を離れるのか

  • ランタイムの長期サポートと最新のパフォーマンス最適化(Kestrel、HTTP/3、非同期 I/O)。
  • モダンな DI/構成/ログの共通化、クラウド・Kubernetes への移行適性。
  • フロントエンド分離(SPA/SSR/ハイブリッド)や API 化の容易さ。
  • ViewState/ポストバック由来の複雑さ・肥大化の解消。

Web Forms → ASP.NET Core 対応表(実務で困る箇所を先回り)

Web FormsASP.NET Core(Razor Pages / MVC / Blazor)移行メモ
Master Page(Site.master)_Layout.cshtmlセクションは RenderSection に置換。コンテンツプレースホルダーはセクション/部分ビューへ。
ユーザーコントロール(.ascx)ViewComponent / Tag Helper / Partial View / Blazor Componentサーバーイベントは削除し、HTTP/JS イベント or コンポーネント境界で扱う。
サーバーコントロール(runat="server")HTML + Tag Helper(asp-for 等)GridView 等はコンポーネント化(ViewComponent/Blazor)か JS グリッドへ。
ViewStateなし(ステートレス)TempData、Hidden、Cookie、Session/分散キャッシュで代替。長期は DB/キャッシュへ。
ポストバック / イベントHTTP メソッド(GET/POST/PUT…)+ Model BindingOnGet/OnPost に集約。コマンドは REST/Action へ明示。
Page Lifecycle(Init/Load/PreRender…)Middleware → Endpoint → PageModel横断関心事は Middleware/Filter。UI 初期化は OnGet。
FormsAuthenticationCookie 認証 + ASP.NET Core Identityロール→クレーム。チケットは DataProtection で保護。
HttpModule / HttpHandlerMiddleware / Endpoint順序はパイプラインで厳密に制御。スレッド静的は排除。
web.configappsettings.json + 環境変数 + SecretIOptions で型安全。変換時に不要キーの棚卸し。
Bundling/Minifyビルド時(Vite/webpack/MSBuild タスク 等)ランタイム依存を解消し、CDN/HTTP/2 に最適化。
OutputCache / Partial CachingResponse Caching / Output Caching / 分散キャッシュキー設計と無効化戦略を明示。

段階移行の設計:止めずに置き換える

プロキシ ブリッジで共存運用

単一ドメイン配下に旧(/legacy)と新(/new)を並べ、リバースプロキシでルーティングします。まず共通ログインを統合し、その後機能ごとに入口 URL を切替える戦略が安全です。

// Program.cs(イメージ)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();

app.MapReverseProxy(); // /legacy などを旧サイトへ委譲
app.MapRazorPages();   // /new を新アプリで提供
app.Run(); 

DNS/ロードバランサはそのまま、アプリ内だけで配線を変更できるため、段階移行と回帰時のコストが最小化されます。

ページ単位の切替とロールアウト

  • アクセスログで低頻度ページから順に移行。
  • Feature Flag(クッキー/ヘッダでのガード)で一部ユーザーへ先行公開。
  • 失敗時はプロキシで旧実装へ即時フォールバック。

UI の再実装:ユーザーコントロールとイベントをどうするか

.ascx → ViewComponent の例

// 旧:/Controls/ProductCard.ascx(抜粋)
// <%@ Control Language="C#" Inherits="Controls.ProductCard" %>
// <asp:Image ID="Img" runat="server" />
// <asp:Label ID="Name" runat="server" />

public class ProductCardViewComponent : ViewComponent
{
private readonly IProductQuery _query;
public ProductCardViewComponent(IProductQuery query) => _query = query;


public async Task<IViewComponentResult> InvokeAsync(int id)
{
    var vm = await _query.GetAsync(id);
    return View("Default", vm); // Views/Shared/Components/ProductCard/Default.cshtml
}


} 
@* Views/Shared/Components/ProductCard/Default.cshtml *@
&lt;article class="product-card"&gt;
  &lt;img src="@Model.ImageUrl" alt="@Model.Name" /&gt;
  &lt;h4&gt;@Model.Name&lt;/h4&gt;
  &lt;span&gt;@Model.Price.ToString("C")&lt;/span&gt;
&lt;/article&gt;

サーバーイベント → OnPost/JS イベント

// Razor Pages のフォーム(Index.cshtml.cs)
public class IndexModel : PageModel
{
    [BindProperty] public AddToCartInput Input { get; set; } = new();
    public IActionResult OnPostAdd()
    {
        // 旧:Button_Click 相当
        // カートへ追加して PRG パターンでリダイレクト
        return RedirectToPage("/Cart", new { ok = true });
    }
}
@* Index.cshtml *@
&lt;form method="post"&gt;
  &lt;input asp-for="Input.ProductId" type="hidden" /&gt;
  &lt;input asp-for="Input.Qty" /&gt;
  &lt;button type="submit" formaction="?handler=Add"&gt;カートに入れる&lt;/button&gt;
&lt;/form&gt;

Tag Helper で HTML を型安全化

// 最少のバリデーション(DataAnnotations)
public record AddToCartInput
{
    [Required] public int ProductId { get; init; }
    [Range(1, 99)] public int Qty { get; init; } = 1;
}
@* ビュー *@
&lt;label asp-for="Input.Qty"&gt;&lt;/label&gt;
&lt;input asp-for="Input.Qty" /&gt;
&lt;span asp-validation-for="Input.Qty"&gt;&lt;/span&gt;

状態管理:ViewState を捨てる代わりに何を使うか

用途代替手段指針
一度だけの成功メッセージTempData(Cookie/Session ベース)PRG パターンと組み合わせる。
短命の UI 状態(タブ選択など)Hidden フィールド、クエリ文字列URL に出してブックマーク可能にする。
ユーザー嗜好/設定Cookie / DB(User Setting)PII は暗号化。期限とサイズを管理。
大きなデータ/集計結果分散キャッシュ(Redis 等)キーにユーザー/バージョンを含める。
サーバーサイドセッションISession + 分散ストア必要最小限に。可用性とスケールを検討。

データアクセスの現実解:既存スキーマを活かしつつ最適化

ADO.NET のまま動かす

// 既存の SqlClient は .NET 8 でも利用可
using var cn = new SqlConnection(cs);
using var cmd = new SqlCommand("dbo.GetOrders", cn) { CommandType = CommandType.StoredProcedure };
cmd.Parameters.AddWithValue("@UserId", userId);
using var rd = await cmd.ExecuteReaderAsync();
// マッピング...

Dapper による薄い置換

var orders = await cn.QueryAsync&lt;Order&gt;(
    "SELECT * FROM dbo.Orders WHERE UserId = @UserId",
    new { UserId = userId });

EF Core:既存 DB(Database First)をそのまま

// スキャフォールドでモデルを生成(例)
// dotnet ef dbcontext scaffold "&lt;接続文字列&gt;" Microsoft.EntityFrameworkCore.SqlServer --data-annotations

テーブル設計を変えない前提でも、トラッキング/非トラッキングや包括的な変更監査・並列性制御を導入できます。移行初期は「読み取りは Dapper、書き込みは ADO.NET/ストアド」の併用でも構いません。最終形を急ぎすぎないことが成功のコツです。

認証・認可の移行:FormsAuth から Identity へ

// Program.cs(抜粋)
builder.Services.AddAuthentication("AppCookie")
    .AddCookie("AppCookie", opt =&gt; { opt.LoginPath = "/auth/login"; });
builder.Services.AddAuthorization(opt =&gt;
{
    opt.AddPolicy("AdminOnly", p =&gt; p.RequireRole("Admin"));
});
  • 移行初期は旧 Cookie を受け入れる「互換モード」を用意(旧→新同時ログイン期間を短期運用)。
  • ロールはクレームへ寄せると柔軟(部署・拠点・等級など)に拡張可能。
  • Anti-forgery は Tag Helper で自動(<form method="post">)。

パイプラインの置換:Global.asax / Module / Handler

// 旧:Global.asax Application_BeginRequest → 新:Middleware
public class CorrelationIdMiddleware
{
    private readonly RequestDelegate _next;
    public CorrelationIdMiddleware(RequestDelegate next) =&gt; _next = next;
    public async Task Invoke(HttpContext ctx)
    {
        ctx.Items["CorrelationId"] = Guid.NewGuid().ToString("N");
        await _next(ctx);
    }
}
// Program.cs
app.UseMiddleware&lt;CorrelationIdMiddleware&gt;();

順序は重要です。認証→権限→エンドポイントの前後関係を明示し、静的クラスの共有状態は排除します(スレッドセーフでないコードは移行の妨げ)。

構成・ログ・テレメトリ:運用で勝つための共通基盤

  • 構成:appsettings.json+環境変数+キーコンテナ(Secret)でレイヤー化。IOptions<T>で型安全。
  • ログ:構造化ログ(JSON)と相関 ID を標準化。重要イベントを監査ログへ二重書き。
  • ヘルスチェック:DB/キャッシュ/外部 API を個別に監視し、LB のプローブで早期切り離し。

ルーティングと URL 設計のやり直し

Web Forms のファイル物理パスから、論理エンドポイント名へ。旧 URL 互換はリダイレクト表で管理します。

// Razor Pages のページハンドラ
// /orders/{id:int}
public class DetailsModel : PageModel
{
    public async Task OnGetAsync(int id) { /* ... */ }
}
// URL リライト(概念)
// /OrderDetail.aspx?id=123 -&gt; 301 -&gt; /orders/123

ビルド/デプロイ:SDK スタイル + コンテナで標準化

  • プロジェクト化:<Project Sdk="Microsoft.NET.Sdk.Web"> へ移行。
  • CI:dotnet restore → dotnet build → dotnet test → dotnet publish。
  • コンテナ:ランタイムと OS 差分を吸収。IIS 運用継続も可能(IIS はリバプロとして)。

よく使うコントロールの移行パターン

旧コントロール新パターン補足
GridViewViewComponent + 部分ビュー/JS グリッド/Blazor Gridソート/ページングを API 化し、クエリで状態管理。
UpdatePanelAJAX(Fetch/HTMX 等)/Partial + フラグメント部分更新の責務を明確化。差分描画で高速化。
CustomValidatorDataAnnotations + IValidatableObject / FluentValidationサーバー・クライアント両方で整合。
FileUploadIFormFile + ストレージ SDKサイズ/拡張子/ウィルススキャンのポリシー化。

Blazor を使う選択の線引き

  • サーバーイベント型の複雑 UI を短工期で維持したい場合は Blazor が近道。
  • SEO が重要・ページ遷移が中心なら Razor Pages がシンプル。
  • 「一覧は Razor Pages、対話は Blazor」というハイブリッドも有効。

落とし穴と対策

  • 動的コントロールの再生成忘れ:Web Forms のライフサイクル依存を捨て、モデルで表現する。
  • 機能凍結違反:旧側に新規コードを足さない。新側のみで実装。
  • セッション肥大:サイズ上限・期限・クリーンアップ方針を先に決める。
  • URL 互換欠落:主要導線は 301 表を準備し、解析タグも移す。
  • JavaScript 依存の崩れ:ID/Name の命名規則変化に注意。フォーム名を固定したい場合は asp-for の生成規約を理解する。

最小実装サンプル:.aspx の典型的フォームを書き換える

旧:OrderEdit.aspx.cs(概念)

protected void SaveButton_Click(object sender, EventArgs e)
{
    var id = int.Parse(Request["id"]);
    var qty = int.Parse(QtyTextBox.Text);
    // 省略...
    Response.Redirect("OrderList.aspx?ok=1");
}

新:Razor Pages(Order/Edit.cshtml.cs)

public class EditModel : PageModel
{
    private readonly IOrderService _svc;
    public EditModel(IOrderService svc) => _svc = svc;


[BindProperty] public EditInput Input { get; set; } = new();

public async Task OnGetAsync(int id) => Input = await _svc.LoadAsync(id);

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid) return Page();
    await _svc.SaveAsync(Input);
    TempData["ok"] = true;
    return RedirectToPage("/Order/List");
}


} 
@* Edit.cshtml(抜粋) *@
&lt;form method="post"&gt;
  &lt;input asp-for="Input.Id" type="hidden" /&gt;
  &lt;input asp-for="Input.Qty" /&gt;
  &lt;span asp-validation-for="Input.Qty"&gt;&lt;/span&gt;
  &lt;button type="submit"&gt;保存&lt;/button&gt;
&lt;/form&gt;

品質を落とさないテスト戦略

  • ユニット:PageModel/サービス・層のロジックを純粋関数として検証。
  • 統合:最重要フロー(認証・支払・検索・帳票)に対して API/Web テスト。
  • 回帰:旧/新の同一入力に対する差分比較(ゴールデンマスター)。

移行プレイブック:この順で進めれば道に迷わない

  1. 棚卸し:URL マップ、依存ライブラリ、DB スキーマ、バッチ、ジョブを全列挙。
  2. 基盤移行:Upgrade Assistant 実行 → SDK プロジェクト化 → ビルドグリーン。
  3. 共通化:レイアウト・ヘッダー/フッター・エラーページ・認証の骨組み確立。
  4. 低頻度ページから:ViewComponent/Tag Helper で部品化しながら置換。
  5. 主要導線へ:Grid/検索/編集を API 化、フロントを段階的に刷新。
  6. 粗利の出る改善:キャッシュ・非同期化・SQL チューニングで体感速度を底上げ。
  7. 清算:プロキシを撤去、レガシーファイルを削除、ドキュメント更新。

セキュリティ/パフォーマンスで外せない実務ポイント

  • CSRF/Clickjacking/XSS のヘッダーを既定有効化(例:Content-Security-Policy)。
  • 非同期化(async/await)でスレッド枯渇を防ぐ。
  • SQL はパラメータ化・タイムアウト・リトライ方針を統一。
  • 画像/静的アセットは長期キャッシュ + バージョン付与。

ミニ FAQ(よくある質問への即答)

Q. 完全自動の変換ツールはある?

A. ありません。Upgrade Assistant はプロジェクト変換と API 置換を助けますが、UI(Web Forms → Razor)は設計判断が必要です。

Q. ViewState/ポストバック/ユーザーコントロールは?

A. ViewState は破棄。ポストバックは OnGet/OnPost+モデルへ。ユーザーコントロールは ViewComponent/Tag Helper/Blazor で再構築します。

Q. 既存 DB は変えずに移行できる?

A. 可能です。最初は既存 ADO.NET/ストアドをそのまま使い、段階的に Dapper/EF Core へ置換すると安全です。

Q. VB.NET のページは?

A. .NET 8 の Web アプリは C# が前提です。変換ツールで雛形を作り、手で整えるのが現実的です。

チェックリスト(貼って使える)

移行前

  • □ URL 一覧・使用頻度・売上寄与を見える化
  • □ web.config のキーと実体利用箇所を棚卸し
  • □ 依存ライブラリの .NET 8 対応可否を確認

各ページ移行時

  • □ UI は ViewComponent/Partial に分割
  • □ PRG パターン+TempData でメッセージ表示
  • □ バリデーションはサーバー/クライアント両面

切替後

  • □ 主要 KPI(TTFB/CLS/エラー率)を監視
  • □ 301 リダイレクト表に漏れがないか確認
  • □ レガシー依存の削除とドキュメント更新

まとめ:移行は「設計再定義」の好機

Web Forms の都合で複雑化した責務を、Razor Pages のシンプルなモデルへ正規化するだけで、読みやすさとテスト容易性が飛躍的に高まります。最短で成果を出す鍵は、①ビルドグリーンを最初のゴールにする、②低リスク領域からページ単位で差し替える、③新規は新側のみで開発する――この三点に尽きます。段階移行を進めながら、パフォーマンス・セキュリティ・運用のベースラインを現代化し、将来の拡張とクラウド適性まで一気に高めましょう。

深掘り:命名・依存・責務の分割ルール

  • PageModel は薄く:入出力定義+ユースケース呼び出しのみ。業務ロジックはサービス層へ。
  • 依存性注入(DI):インターフェースを境界に分け、テストダブルで検証可能に。
  • 入出力 DTO を明示:フォーム入力をエンティティへ直接結びつけない。
  • 非同期ファースト:I/O すべて async/await。同期ブロッキング禁止。

国際化/帳票/バッチ:周辺機能の扱い

  • 多言語:リソースは .resx を ResourceManager で継続利用可。カルチャはミドルウェアで設定。
  • 帳票:既存ライブラリが .NET 8 対応か要確認。非対応ならサーバーレス/コンテナで別プロセス化。
  • バッチ:IHostedService/Quartz でスケジュール。Web とプロセスを分離すると安定。

パフォーマンス Tuning の初手

  1. 遅い SQL の上位 10 件を直す(索引/Include/スキャン迂回)。
  2. 検索一覧を NoTracking + ページング固定(上限 100)。
  3. 共通部品化で HTML 重複を削り、TTFB を短縮。
  4. 画像/静的配信を CDN 化し、圧縮と HTTP/2 を活用。

サンプル:Tag Helper で「必須ラベル+入力」を共通化

public class RequiredLabelTagHelper : TagHelper
{
    public ModelExpression For { get; set; } = default!;
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "label";
        output.Attributes.SetAttribute("for", For.Name);
        output.Content.SetHtmlContent($"{For.Metadata.DisplayName}&lt;span class='req'&gt;*&lt;/span&gt;");
    }
}
@* 使い方 *@
&lt;required-label for="Input.Qty"&gt;&lt;/required-label&gt;
&lt;input asp-for="Input.Qty" /&gt;

移行の費用対効果を説明するための指標

KPI定義目標例
TTFB最初のバイトまでの時間< 200ms(主要ページ)
エラー率5xx/全リクエスト< 0.1%
変更リードタイムマージ〜本番反映< 1 日
回帰発生率本番障害/リリース< 5%

最後に:チームの合意形成テンプレート

  • 目的:LTS/性能/開発速度/セキュリティの改善
  • スコープ:URL 群・DB・外部連携・帳票
  • 非スコープ:旧側への新機能追加・低優先レガシーの最適化
  • 段階:基盤→共通→ページ置換→切替→清算
  • 成功条件:主要 KPI 達成、301 完了、障害ゼロリリース 3 回連続

この記事を書いた人

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

コメント

コメントする

目次