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 WebAssembly | 100% クライアント。オフライン可。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 に合わせて明示化。 |
| UpdatePanel | Partial Render(Razor)/ Fetch + DOM / Blazor のリアクティブ更新 | 「ポストバックの擬似化」を卒業。 |
| HttpHandler/Module | Middleware / Endpoint | パイプライン順序が設計の肝。 |
| Global.asax | Program.cs / Startup 構成 | DI/Options/Logging を標準化。 |
| Web.config(設定) | appsettings.json + 環境別 JSON + Secret | 型付き Options で安全化。 |
| Membership/FormsAuth | ASP‑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");
}
}
<!-- Pages/Customers/Create.cshtml -->
@page
@model CreateModel
<form method="post">
<input asp-for="Input.Name" />
<span asp-validation-for="Input.Name"></span>
<button type="submit">保存</button>
</form>
GridView 相当(Razor Pages + Tag Helper)
// Pages/Customers/Index.cshtml.cs
public class IndexModel(ICustomerQuery query) : PageModel
{
public IReadOnlyList<CustomerDto> Items { get; private set; } = Array.Empty<CustomerDto>();
public async Task OnGetAsync() => Items = await query.GetPageAsync(1, 50);
}
<table class="table">
<thead><tr><th>ID</th><th>名前</th><th>操作</th></tr></thead>
<tbody>
@foreach (var x in Model.Items)
{
<tr>
<td>@x.Id</td>
<td>@x.Name</td>
<td><a asp-page="Edit" asp-route-id="@x.Id">編集</a></td>
</tr>
}
</tbody>
</table>
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<AppOptions>(builder.Configuration.GetSection("App"));
// 取得: var opt = app.Services.GetRequiredService<IOptions<AppOptions>>().Value;
認証・認可の移行(Membership → Identity / OIDC)
最もトラブルになりやすい領域です。現実解は 2 択:
- ASP‑NET Core Identity へ移行(ユーザー・ロール・クレームを再設計)。
- 外部 IdP(OpenID Connect) へ寄せ、旧 Web Forms と新 .NET 8 の双方を同じ IdP にぶら下げて SSO。
パスワードはハッシュ方式が異なるため、以下のいずれかを選びます。
- ユーザー初回ログイン時に旧ハッシュを検証 → 新方式に再ハッシュ(lazy migration)。
- 一括移行バッチで新ハッシュへ変換(不可なら暫定的に二重運用)。
ロール/権限は「ページ単位の許可」から「ポリシー(クレーム)駆動」へ置き換えます。
セッションと状態管理の見直し
セッションは「必要最小限」に絞ります。分散環境では IDistributedCache(例:SQL Server/Redis)を使います。Blazor Server ではコンポーネント状態がメモリに乗るため、セッション依存を減らし、復元可能な 再構築戦略(DB/キャッシュ)を設計してください。
builder.Services.AddDistributedSqlServerCache(o => {
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 => c.IsActive)
.Include(c => c.Orders.Where(o => o.Total >= 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) => await q.GetTopAsync())
.CacheOutput(p => 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/API | WebApplicationFactory / FluentAssertions | ルーティング・フィルタ・認証 |
| UI | Playwright / 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<SmtpOptions>(builder.Configuration.GetSection("Smtp"));
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
Serilog/NLog など外部ロガーの橋渡し
builder.Host.UseSerilog((ctx, lc) => lc.ReadFrom.Configuration(ctx.Configuration));
静的ファイルとキャッシュ制御
app.UseStaticFiles(new StaticFileOptions {
OnPrepareResponse = ctx => {
var headers = ctx.Context.Response.GetTypedHeaders();
headers.CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue {
Public = true, MaxAge = TimeSpan.FromDays(7)
};
}
});
ファイルアップロード(大容量対応)
builder.WebHost.ConfigureKestrel(o => o.Limits.MaxRequestBodySize = 52428800); // 50MB
app.MapPost("/upload", async (HttpRequest req, IWebHostEnvironment env) =>
{
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
<tr><td>@Model.Id</td><td>@Model.Name</td></tr>
<tbody id="rows">
@foreach (var x in Model.Items) { @await Html.PartialAsync("_CustomerRow", x) }
</tbody>
<script>
// fetch で差分 HTML を受けて #rows に挿入するシンプル更新
</script>
最後に:意思決定の指針
- UI は「作り直し」だが、ビジネスロジックは「移設」。
- 段階移行(YARP)はダウンタイムを避け、機能ごとの投資対効果を測れる。
- 認証・状態・データの設計を先に解くと、残りは手数の勝負になる。

コメント