ASP.NET Web Formsを.NET 8へ移行する実践ガイド|Blazor/Razor PagesとYARPで段階移行する手順と注意点

ASP‑NET Web Forms を運用中の組織にとって、.NET 8(ASP‑NET Core)への移行は「作り直し」と「資産再利用」の綱引きです。本記事は、Web Forms 非対応という制約の中で、再利用できるロジックを最大化しつつ UI を計画的に置き換えるための実践ガイドです。段階移行、YARP の併用、Blazor/Razor Pages での再実装パターンを、コードと表で具体化します。

目次

背景と結論

ASP‑NET Web Forms(.NET Framework 4.7.1)は .NET 8 でサポートされません。そのため UI は全面的に書き直しが前提です。一方で、ビジネスロジック/データアクセス層は切り出して再利用できます。最短距離は次の方針です。

  • 非 UI の共有資産を .NET Standard 2.0 に移行して両環境で共用 → のちに .NET 8 へ切替。
  • UI は Blazor Server または Razor Pages で再実装。
  • 旧・新アプリは YARP リバースプロキシで同一ドメインに統合し、段階置換。
  • 認証は ASP‑NET Core Identity または外部 IdP(OpenID Connect)へ。
  • UI 全面改修が難しければ一旦 .NET Framework 4.8.1 に上げて延命。

質問概要と要点(再掲)

目的:既存の C# ASP‑NET Web Forms を .NET 8(ASP‑NET Core)へアップグレード。課題:Web Forms は .NET 8 非対応、Upgrade Assistant でも変換不可。知りたいこと:現実的な移行手順と活用できるツール。

回答・解決策の全体像

ステップ内容補足
1. 全体分析画面(UI)とビジネスロジック/データアクセスを切り分け。Web Forms 専用コードを特定し、非 UI をクラスライブラリ化。規模把握・工数見積の基礎。
2. 共有コード移行抽出ライブラリを .NET Standard 2.0 化(両環境で併用)→最終的に .NET 8 へ。VB で書かれた UI は Core に直載せ不可。必要なら C# 化。
3. 新 UI 選定Razor Pages / Blazor Server / Blazor WASM から選ぶ。Web Forms 的体験は Blazor Server が最も近い。
4. UI 再実装.aspx を順次置換。優先度高い画面から段階リライト。ViewState/サーバーコントロール/UpdatePanel は廃止。
5. インクリメンタル移行YARP で旧新共存。「1 つのサイト」に見せて徐々に切替。認証は SSO(IdP)共有が現実的。セッションは機能ごとに整理。
6. 認証・依存更新Membership → ASP‑NET Core Identity。サードパーティは .NET 8 対応へ。パスワード移行方針を早期に決める。
7. テスト & 検証機能/性能/互換性/負荷。特に状態管理と SignalR 負荷。CI/CD とステージングで段階検証。
8. デプロイWindows/IIS・Linux/Nginx・Docker などへ発行。コンテナ化でマルチ OS へ展開容易。
9. 代替案.NET Framework 4.8.1 へ上げて延命。OS サポート範囲に依存。

技術選定:Razor Pages / Blazor / MVC の比較

選択肢特長向き/不向き移行コスト
Razor Pagesページ指向。学習容易。サーバーラウンドトリップ中心。シンプル CRUD、管理画面、フォーム中心。中(コントロール置換は必要)
Blazor Serverイベント駆動・コンポーネント指向。Web Forms に近い体験。SignalR 常時接続。リッチ UI、細かい双方向操作、ポストバック的 UX。低〜中(概念親和性が高い)
Blazor WebAssembly100% クライアント。オフライン可。CDN 展開◎。SPA、API 分離が明確な構成。中〜高(API 再設計が必要になりがち)
ASP‑NET Core MVC従来 MVC の延長。移行枠として有効。MVC 5 からの橋渡しに限定的に有効。中

Web Forms 機能の対応表

Web Forms の概念.NET 8 での置き換えポイント
ViewState不要/最小化。状態は TempData、隠しフィールド、DB/キャッシュ、Blazor のコンポーネント状態へ。ペイロード削減で高速化。設計見直しの好機。
サーバーコントロールTag Helper / HTML + JS / Blazor コンポーネントイベントは HTTP に合わせて明示化。
UpdatePanelPartial Render(Razor)/ Fetch + DOM / Blazor のリアクティブ更新「ポストバックの擬似化」を卒業。
HttpHandler/ModuleMiddleware / Endpointパイプライン順序が設計の肝。
Global.asaxProgram.cs / Startup 構成DI/Options/Logging を標準化。
Web.config(設定)appsettings.json + 環境別 JSON + Secret型付き Options で安全化。
Membership/FormsAuthASP‑NET Core Identity / 外部 IdP(OIDC)パスワード移行戦略が最重要。

全体分析:再利用資産の抽出と棚卸し

最初の 2〜4 週間は「作らない」仕事に集中します。重要なのは UI 依存のない純ロジックの抽出と、Web Forms 固有依存の洗い出しです。

  • 対象コード量、画面数、ユーザー数(同時接続)、サードパーティ依存(PDF、帳票、グリッド、チャート)。
  • Web Forms 固有 API の検索:ViewState、PostBack、Server.Transfer、UpdatePanel、GridView、ObjectDataSource、HttpContext.Current、Session 直参照、HttpModule/HttpHandler。
  • DB アクセス(EF6/ADO.NET/ストアド)、トランザクション境界、分散トランザクションの有無。
  • 認証(Membership/FormsAuth/OAuth/ADFS)、ロール/権限の付与方式。
  • パフォーマンス課題(ViewState 過大、PostBack 頻度、同期 IO)。

.NET Standard 2.0 への移行方針

非 UI の共通ライブラリは、まず .NET Standard 2.0 に集約します。これにより旧 Web Forms と新 .NET 8 の両方から参照でき、段階移行が成立します。プロジェクトファイルの例:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
    <Nullable>enable</Nullable>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>
  <ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="<latest>" />
  </ItemGroup>
</Project>

UI から 静的参照していたクラスは DI(依存性注入)で受け取る形にリファクタリングします。これは後述のテスト自動化にも効きます。

UI 再実装パターン(Razor Pages / Blazor)

フォーム(Web Forms → Razor Pages)

// Pages/Customers/Create.cshtml.cs
public class CreateModel : PageModel
{
    private readonly ICustomerService _svc;
    public CreateModel(ICustomerService svc) => _svc = svc;


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

public void OnGet() { }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid) return Page();
    await _svc.CreateAsync(Input);
    TempData["msg"] = "作成しました";
    return RedirectToPage("./Index");
}


} 
&lt;!-- Pages/Customers/Create.cshtml --&gt;
@page
@model CreateModel
&lt;form method="post"&gt;
  &lt;input asp-for="Input.Name" /&gt;
  &lt;span asp-validation-for="Input.Name"&gt;&lt;/span&gt;
  &lt;button type="submit"&gt;保存&lt;/button&gt;
&lt;/form&gt;

GridView 相当(Razor Pages + Tag Helper)

// Pages/Customers/Index.cshtml.cs
public class IndexModel(ICustomerQuery query) : PageModel
{
    public IReadOnlyList&lt;CustomerDto&gt; Items { get; private set; } = Array.Empty&lt;CustomerDto&gt;();
    public async Task OnGetAsync() =&gt; Items = await query.GetPageAsync(1, 50);
}
&lt;table class="table"&gt;
  &lt;thead&gt;&lt;tr&gt;&lt;th&gt;ID&lt;/th&gt;&lt;th&gt;名前&lt;/th&gt;&lt;th&gt;操作&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
  &lt;tbody&gt;
  @foreach (var x in Model.Items)
  {
    &lt;tr&gt;
      &lt;td&gt;@x.Id&lt;/td&gt;
      &lt;td&gt;@x.Name&lt;/td&gt;
      &lt;td&gt;&lt;a asp-page="Edit" asp-route-id="@x.Id"&gt;編集&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
  }
  &lt;/tbody&gt;
&lt;/table&gt;

UpdatePanel 相当(Blazor Server)

@page "/counter"
@inject ICounterService Svc

カウンター
現在: @count
+1

@code {
int count;
protected override async Task OnInitializedAsync()
=> count = await Svc.GetAsync();

async Task Increment()
{
count++;
await Svc.SaveAsync(count);
}
} 

Blazor Server はイベント駆動で、Web Forms の コントロール+イベント モデルに近い感覚で実装できます。SignalR による常時接続のため、同時接続数×メモリの試算とスケール計画は必須です。

ミドルウェア移行:HttpHandler/Module → Middleware

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// 旧: HttpModule でのロギング/監査
// 新: Middleware にリライト
app.Use(async (ctx, next) =>
{
var sw = Stopwatch.StartNew();
await next();
sw.Stop();
app.Logger.LogInformation("{Path} {Status} {Elapsed}ms",
ctx.Request.Path, ctx.Response.StatusCode, sw.ElapsedMilliseconds);
});

app.MapGet("/", () => "OK");
app.Run(); 

設定移行:Web.config → appsettings.json

{
  "ConnectionStrings": {
    "Default": "Server=.;Database=AppDb;Trusted_Connection=True;TrustServerCertificate=True"
  },
  "App": {
    "ReportPath": "C:/Reports"
  },
  "Logging": {
    "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" }
  }
}
builder.Services.Configure&lt;AppOptions&gt;(builder.Configuration.GetSection("App"));
// 取得: var opt = app.Services.GetRequiredService&lt;IOptions&lt;AppOptions&gt;&gt;().Value;

認証・認可の移行(Membership → Identity / OIDC)

最もトラブルになりやすい領域です。現実解は 2 択:

  • ASP‑NET Core Identity へ移行(ユーザー・ロール・クレームを再設計)。
  • 外部 IdP(OpenID Connect) へ寄せ、旧 Web Forms と新 .NET 8 の双方を同じ IdP にぶら下げて SSO。

パスワードはハッシュ方式が異なるため、以下のいずれかを選びます。

  1. ユーザー初回ログイン時に旧ハッシュを検証 → 新方式に再ハッシュ(lazy migration)。
  2. 一括移行バッチで新ハッシュへ変換(不可なら暫定的に二重運用)。

ロール/権限は「ページ単位の許可」から「ポリシー(クレーム)駆動」へ置き換えます。

セッションと状態管理の見直し

セッションは「必要最小限」に絞ります。分散環境では IDistributedCache(例:SQL Server/Redis)を使います。Blazor Server ではコンポーネント状態がメモリに乗るため、セッション依存を減らし、復元可能な 再構築戦略(DB/キャッシュ)を設計してください。

builder.Services.AddDistributedSqlServerCache(o =&gt; {
  o.ConnectionString = builder.Configuration.GetConnectionString("Default");
  o.SchemaName = "dbo"; o.TableName = "Cache";
});
builder.Services.AddSession();
app.UseSession();

データアクセス:EF6 → EF Core(または ADO.NET 継続)

  • 手動移行の勘所:遅延読み込み(Lazy Loading)、クエリ翻訳の差、トラッキング動作。
  • 大量バルク更新は Set-based に見直す(ExecuteUpdate などの活用)。
  • トランザクション境界と SaveChanges の回数を最適化。
// EF Core: Include で明示ロード
var orders = await db.Customers
    .Where(c =&gt; c.IsActive)
    .Include(c =&gt; c.Orders.Where(o =&gt; o.Total &gt;= 10000))
    .ToListAsync();

既存が ADO.NET ならそのまま再利用し、接続管理と非同期化(async/await)だけでも大きく改善します。

インクリメンタル移行:YARP による共存運用

「見た目は 1 つのサイト」を守りながら、ページ単位で置き換えます。ルーティング例:

// appsettings.json(抜粋)
{
  "ReverseProxy": {
    "Routes": {
      "legacy": { "ClusterId": "legacy", "Match": { "Path": "/legacy/{**catch-all}" } },
      "newapp": { "ClusterId": "newapp", "Match": { "Path": "/{**catch-all}" } }
    },
    "Clusters": {
      "legacy": { "Destinations": { "d1": { "Address": "http://localhost:8080/" } } },
      "newapp": { "Destinations": { "d1": { "Address": "http://localhost:5005/" } } }
    }
  }
}
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();

認証は Cookie のバイナリ互換を狙うより、共通 IdP(OpenID Connect) で SSO を構成する方が安全・確実です。ユーザー体験は維持しつつ、裏側で徐々に新 UI へルートを切り替えていきます。

パフォーマンス最適化(.NET 8)

  • Output Cache(動的出力キャッシュ)で読み取り主体ページのスループットを向上。
  • Response Compression と HTTP/2/HTTP/3、有効な Keep-Alive、ヘッダー最適化。
  • 非同期 IO 徹底、同期ブロッキングの排除、接続プールの適正化。
builder.Services.AddOutputCache();
app.UseOutputCache();
app.MapGet("/products", async (IProductQuery q) =&gt; await q.GetTopAsync())
   .CacheOutput(p =&gt; p.Expire(TimeSpan.FromMinutes(5)));

セキュリティ強化ポイント

  • Antiforgery(Razor)/CSP/X-Content-Type-Options/X-Frame-Options。
  • データ保護キーの外部保存(クラスタ構成では必須)。
  • 秘密情報は Secret Manager / Key Vault 等で分離。

デプロイ:IIS / Nginx / コンテナ

  • Windows:IIS + AspNetCoreModuleV2(Kestrel を背面配置)。
  • Linux:Nginx/Apache のリバースプロキシ。systemd で常駐。
  • Docker:CI でビルド → 脆弱性スキャン → マルチステージで軽量化。
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /out
FROM base AS final
WORKDIR /app
COPY --from=build /out .
ENTRYPOINT ["dotnet","App.dll"]

テスト戦略

層フレームワーク狙い
ユニットxUnit / NUnit / MSTestロジック再利用の安全性担保
Web/APIWebApplicationFactory / FluentAssertionsルーティング・フィルタ・認証
UIPlaywright / bUnit(Blazor)主要シナリオの回帰防止
負荷k6 / JMeter同時接続・SignalR の頭打ち確認

落とし穴と対策

  • ViewState 依存:状態を再設計。コンポーネント内に閉じない重要状態は DB/キャッシュで管理。
  • UpdatePanel 多用:差分更新は Blazor で素直に、または局所的に fetch + DOM。
  • HttpContext.Current:DI 経由の IHttpContextAccessor を使用。
  • 同期 DB 呼び出し:すべて非同期化。非同期でない外部 SDK はラップして隔離。
  • 巨大な God Page:Razor/Blazor コンポーネントに分割。RenderFragment で再利用。

既存資産を最大活用する具体テクニック

  • 共通 UI(表や入力部品)は Razor Class Library や Blazor コンポーネントに切り出し、複数アプリで再利用。
  • 帳票・PDF はサーバー側生成を継続し、ダウンロードエンドポイントを提供。
  • フロントは段階的にモダン化(まずは Tag Helper、必要になった箇所から TypeScript/モジュール化)。

プロジェクト計画:90 日ロードマップ

期間成果物メトリクス
0–30 日棚卸し・非 UI 切出し・.NET Standard 化・PoC(Blazor/Razor)再利用率、PoC 性能、主要画面の見積精度 ±20%
31–60 日YARP で共存開始、優先 10 画面を新 UI へ、認証の新基盤を立てる切替済みページ比率、障害ゼロ、ユーザー影響なし
61–90 日性能・負荷テスト、残タスクのファクト化、CI/CD 安定化スループット +X%、p95 レイテンシ目標達成

見積りの考え方

項目係数の目安備考
画面あたり実装2〜8 人日UI 複雑度・バリデーション・グリッド機能で増減
共通基盤10〜20 人日認証/認可、エラーハンドリング、レイアウト、ログ、検査
データアクセス移行0.5〜2 人日/エンティティEF Core への調整や SQL 再設計の有無
負荷対策5〜15 人日SignalR の接続/スケール戦略含む

品質・運用の作り込み

  • ログ:構造化ログ(リクエスト ID、ユーザー、業務キー)。
  • メトリクス:リクエスト/秒、エラー率、p95・p99、接続数、GC 回数。
  • ヘルスチェック:DB/Cache/外部 API の応答監視。

段階移行チェックリスト

  • 非 UI ロジックの .NET Standard 化が完了している。
  • UI 再実装の指針(Razor/Blazor)が決まり、サンプル 2 画面が完了。
  • YARP による共存運用がステージングで安定。
  • 認証/認可の新基盤が本番相当で稼働。
  • 性能目標(p95、同時接続)が実測で達成。
  • 運用手順(リリース、ロールバック、監視)が整備済み。

代替案:.NET Framework 4.8.1 での延命

新 UI の全面書き直しが投資対効果に見合わない場合、OS サポート期間に合わせて .NET Framework 4.8.1 へ上げる選択も現実的です。並行して非 UI を .NET Standard 化しておけば、将来の .NET 8 以降への再挑戦が容易になります。

まとめ

Web Forms から .NET 8 への移行は、UI の再実装と資産再利用の最大化のトレードオフです。最初にロジックを .NET Standard 2.0 に寄せ、UI は Blazor/Razor Pages で再構築、YARP で段階切替するのが最短の成功パターンです。認証・状態管理・データアクセスの 3 点を早期に設計できれば、移行リスクは大きく下がります。最後は、測れるメトリクス(性能・品質・切替比率)で前進を可視化してください。


補足:よく使うコードスニペット集

Options パターン

builder.Services.Configure&lt;SmtpOptions&gt;(builder.Configuration.GetSection("Smtp"));
builder.Services.AddSingleton&lt;IEmailSender, SmtpEmailSender&gt;();

Serilog/NLog など外部ロガーの橋渡し

builder.Host.UseSerilog((ctx, lc) =&gt; lc.ReadFrom.Configuration(ctx.Configuration));

静的ファイルとキャッシュ制御

app.UseStaticFiles(new StaticFileOptions {
    OnPrepareResponse = ctx =&gt; {
        var headers = ctx.Context.Response.GetTypedHeaders();
        headers.CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue {
            Public = true, MaxAge = TimeSpan.FromDays(7)
        };
    }
});

ファイルアップロード(大容量対応)

builder.WebHost.ConfigureKestrel(o =&gt; o.Limits.MaxRequestBodySize = 52428800); // 50MB
app.MapPost("/upload", async (HttpRequest req, IWebHostEnvironment env) =&gt;
{
    var file = req.Form.Files.First();
    var path = Path.Combine(env.ContentRootPath, "uploads", file.FileName);
    await using var fs = File.Create(path);
    await file.CopyToAsync(fs);
    return Results.Ok();
});

ヘルスチェック

builder.Services.AddHealthChecks().AddSqlServer(
    builder.Configuration.GetConnectionString("Default"));
app.MapHealthChecks("/health");

Razor 部分レンダリング(部分更新)

// 部品化: Pages/Shared/_CustomerRow.cshtml
@model CustomerDto
&lt;tr&gt;&lt;td&gt;@Model.Id&lt;/td&gt;&lt;td&gt;@Model.Name&lt;/td&gt;&lt;/tr&gt;
&lt;tbody id="rows"&gt;
  @foreach (var x in Model.Items) { @await Html.PartialAsync("_CustomerRow", x) }
&lt;/tbody&gt;
&lt;script&gt;
// fetch で差分 HTML を受けて #rows に挿入するシンプル更新
&lt;/script&gt;

最後に:意思決定の指針

  • UI は「作り直し」だが、ビジネスロジックは「移設」。
  • 段階移行(YARP)はダウンタイムを避け、機能ごとの投資対効果を測れる。
  • 認証・状態・データの設計を先に解くと、残りは手数の勝負になる。

この記事を書いた人

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

コメント

コメントする

目次