ASP.NET Web フォーム(.aspx) + MVP で作られた .NET Framework 4.7 アプリを、最新の .NET 8(ASP.NET Core)へ最短・安全に移行するための実践ガイドです。Upgrade Assistant では UI を自動変換できません。本稿では現実的な移行先の選定、段階移行の手順、具体コード、落とし穴回避までを一気通貫で解説します。
結論:Web フォームは ASP.NET Core で非サポート(UI は手動で書き換え)
ASP.NET Core には Web フォーム(.aspx)ランタイムがありません。Upgrade Assistant はクラス ライブラリやプロジェクト ファイルの近代化には有効ですが、.aspx/.ascx の UI を自動変換することはできません。つまり、UI 層は Razor Pages / MVC / Blazor のいずれかへ書き換えが必須です。一方で、MVP の Presenter やドメインロジックは切り出して再利用できます。
できること/できないこと
| 項目 | 状態 | 補足 |
|---|---|---|
| .aspx/.ascx の自動変換 | できない | UI は新フレームワークへ手動で移植 |
| Presenter/ビジネスロジックの再利用 | できる | .NET Standard または .NET 8 対応のクラス ライブラリへ |
| Web.config の移行 | 部分的 | appsettings.json + Options パターンへ再設計 |
| Membership Provider | 置き換え | ASP.NET Identity(Cookie/JWT)へ |
| Global.asax / HttpModules | 置き換え | Program.cs + Middleware パイプラインへ |
移行先 UI 技術の選び方(Razor Pages / MVC / Blazor)
画面数、複雑度、将来の保守方針に合わせて選びます。最短での移行が目的なら Razor Pages が第一候補です。API 共存や高度なルーティングが重要なら MVC、C# だけでリッチ UI を求めるなら Blazor を検討します。
| 移行パス | 特徴 | 向いているケース |
|---|---|---|
| Razor Pages | 1 ページ = 1 ページモデル。コードビハインドの移植が容易。 | CRUD 中心、画面数が多い、段階移行でスピード重視 |
| MVC | Controller と View の分離が明確。テスト容易。 | 複雑なルーティング、Web API との共存や拡張性重視 |
| Blazor Server / WebAssembly | C# 中心に SPA を実現。Presenter ロジックの再利用性高い。 | リッチな対話 UI、JS 依存を減らしたい、リアルタイム性 |
実務で使える「段階移行」ロードマップ(安全に止めずに進める)
- 整理・設計:画面一覧、依存関係、データソース、認証方式、URL を棚卸しし、優先度と投入順を決定。
- ロジック分離:Presenter とドメインロジックを
.NET Standard 2.0/2.1あるいは.NET 8のクラスライブラリへ抽出。単体テストを追加。 - 非 UI ライブラリの先行移行:Upgrade Assistant 等で共通ライブラリを .NET 8 化し、ビルド & テストを安定化。
- UI の書き換え:Razor Pages/MVC/Blazor へ順次移植。ViewState/ポストバックはモデルバインディングへ。
- インフラ更新:認証(ASP.NET Identity)、構成(appsettings.json)、DI、ログ、キャッシュ、セッション、ミドルウェアに置換。
- 併用・カナリア:IIS や YARP でリバースプロキシを構成し、旧/新サイトをパス単位で共存。
- 切替・縮退:アクセスを段階的に新サイトへ寄せ、エラーレート・パフォーマンス監視を通過後に旧画面を縮退。
概算の目安(粗い見積り式)
| 画面タイプ | 移行難易度 | 標準工数の目安 | 備考 |
|---|---|---|---|
| 単純 CRUD(一覧+登録/編集) | 低 | 0.5〜1.5 人日/画面 | Razor Pages 推奨 |
| 複雑検索・ウィザード | 中 | 2〜4 人日/画面 | 状態管理の再設計が鍵 |
| Ajax/UpdatePanel 依存 | 中〜高 | 3〜6 人日/画面 | 非同期 UI の再構築(部分更新 or Blazor) |
| カスタムサーバーコントロール | 高 | 5 人日〜/画面 | Tag Helper / ViewComponent / Blazor で代替 |
MVP から新 UI へのマッピング早見表
| Web フォーム(MVP) | ASP.NET Core(推奨) | メモ |
|---|---|---|
| Page(.aspx) + CodeBehind | Razor Page + PageModel | イベント→ハンドラ(OnGet/OnPost)に集約 |
| Presenter | DI された Application Service | インターフェース経由で注入 |
| ViewState / PostBack | ModelBinding / TempData / Hidden | 必要最小限の状態のみ保持 |
| Server Controls(GridView 等) | Tag Helper / 部分ビュー / コンポーネント | 描画とロジックを分離 |
| UserControl(.ascx) | Partial View / ViewComponent / Blazor Component | 再利用単位を明確化 |
| MasterPage | _Layout.cshtml | セクションで拡張 |
| Global.asax / HttpModule | Program.cs / Middleware | 順序と委譲が重要 |
| Membership Provider | ASP.NET Identity(Cookie/JWT) | 段階移行なら一時ブリッジも可 |
| Web.config | appsettings.json + Options | 環境ごとに分割・上書き |
コードで理解する:Web フォームから Razor Pages への移植例
Program.cs(最小ホスティング)
using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews(); // MVC も使う場合
builder.Services.AddScoped();
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o => o.LoginPath = "/Account/Login");
builder.Services.AddAuthorization();
builder.Services.AddSession(); // 必要なら
builder.Services.AddOutputCache(); // .NET 8 の出力キャッシュ
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.UseOutputCache();
app.MapRazorPages();
app.MapDefaultControllerRoute();
app.Run();
PageModel(Web フォームの Page_Load/ボタンクリック相当)
public record CustomerInput
{
[Required, StringLength(100)]
public string Name { get; init; } = string.Empty;
}
public class IndexModel : PageModel
{
private readonly ICustomerService _service;
public IndexModel(ICustomerService service) => _service = service;
public IReadOnlyList<CustomerDto> Customers { get; private set; } = Array.Empty<CustomerDto>();
[BindProperty]
public CustomerInput Input { get; set; } = new();
public async Task OnGetAsync()
{
Customers = await _service.GetAllAsync();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
Customers = await _service.GetAllAsync();
return Page();
}
await _service.CreateAsync(Input);
TempData["Flash"] = "保存しました";
return RedirectToPage(); // PRG パターン
}
}
Razor(Tag Helper でサーバーコントロールを置換)
@page
@model IndexModel
@if (TempData["Flash"] is string msg)
{
@msg
}
保存
@foreach (var c in Model.Customers)
{
}
@c.Name
MVC での書き換え最小例(Controller + View)
public class CustomersController : Controller
{
private readonly ICustomerService _service;
public CustomersController(ICustomerService service) => _service = service;
[HttpGet]
public async Task<IActionResult> Index() => View(await _service.GetAllAsync());
[HttpPost]
public async Task<IActionResult> Create(CustomerInput input)
{
if (!ModelState.IsValid) return View("Index", await _service.GetAllAsync());
await _service.CreateAsync(input);
TempData["Flash"] = "保存しました";
return RedirectToAction(nameof(Index));
}
}
@model IEnumerable<CustomerDto>
@if (TempData["Flash"] is string f) { <div class="alert">@f</div> }
<form asp-action="Create" method="post">
<input name="Name" />
<button type="submit">追加</button>
</form>
<ul>@foreach (var c in Model) { <li>@c.Name</li> }</ul>
Blazor で Presenter ロジックを再利用する例
@page "/customers"
@inject ICustomerService Service
保存
@if (Customers is null)
{
Loading...
}
else
{
@foreach (var c in Customers) { @c.Name }
}
@code {
private CustomerInput Input = new();
private List? Customers;
protected override async Task OnInitializedAsync()
=> Customers = (await Service.GetAllAsync()).ToList();
private async Task SaveAsync()
{
await Service.CreateAsync(Input);
Input = new();
Customers = (await Service.GetAllAsync()).ToList();
}
}
Web フォーム固有機能の置き換え戦略
| 旧来の機能 | 置き換え先 | 注意点 |
|---|---|---|
| ViewState | モデル、Hidden、TempData、セッション | 極力サーバー側の再計算へ。過度な状態保存は避ける。 |
| PostBack イベント | HTTP メソッド + ハンドラ(OnPost/Action) | PRG(Post-Redirect-Get)で再投稿防止 |
| UpdatePanel | 部分ビュー、fetch、Blazor の部分更新 | 通信単位と UI 再描画範囲を明確化 |
| GridView/Repeater | HTML テーブル + ループ、コンポーネント | ソート/ページングはサーバー側 API 化 |
| Validators | DataAnnotations + クライアント検証 | サーバー側での再検証は必須 |
認証・認可の移行(Membership → Identity)
新規は ASP.NET Identity(Cookie/JWT)を採用します。既存ユーザーを段階的に移すなら「ログイン時移行」戦略が安全です。
- Identity スキーマで新テーブルを用意。
- ログイン要求が来たら旧ユーザー テーブルで検証。
- 成功したら Identity にユーザーを作成し、以後は新側で認証。
// Cookie 認証(概要)
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o => {
o.LoginPath = "/Account/Login";
o.AccessDeniedPath = "/Account/Denied";
o.SlidingExpiration = true;
});
builder.Services.AddAuthorization();
構成・DI・ロギングのベストプラクティス
appsettings.json と Options パターン
{
"ConnectionStrings": { "Default": "Server=...;Database=..." },
"FeatureFlags": { "UseNewSearch": true }
}
builder.Services.Configure<FeatureFlags>(builder.Configuration.GetSection("FeatureFlags"));
public sealed class FeatureFlags { public bool UseNewSearch { get; init; } }
依存性注入(Presenter → アプリケーションサービス)
public interface IOrderService
{
Task<OrderDto> GetAsync(int id);
Task CreateAsync(CreateOrder command);
}
builder.Services.AddScoped();
ロギング
public class OrderService : IOrderService
{
private readonly ILogger<OrderService> _log;
public OrderService(ILogger<OrderService> log) => _log = log;
public async Task<OrderDto> GetAsync(int id)
{
_log.LogInformation("Get order {Id}", id);
// ...
return await Task.FromResult(new OrderDto());
}
}
状態管理・キャッシュ・出力キャッシュ
// セッション(必要な場合のみ)
builder.Services.AddDistributedSqlServerCache(o => {
o.ConnectionString = builder.Configuration.GetConnectionString("Default");
o.SchemaName = "dbo";
o.TableName = "Cache";
});
builder.Services.AddSession();
app.UseSession();
// 出力キャッシュ(.NET 8)
builder.Services.AddOutputCache(o => o.AddPolicy("anon", b => b.Expire(TimeSpan.FromMinutes(1))));
app.UseOutputCache();
app.MapGet("/api/products", (IProductService s) => s.GetAsync()).CacheOutput("anon");
ルーティング・URL 設計とリバースプロキシ(併用運用)
旧 Web フォームを動かしつつ、新機能だけ ASP.NET Core で提供する場合は、IIS や YARP を用いてパスごとに振り分けます。
// Program.cs(一例)
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
app.MapReverseProxy();
{
"ReverseProxy": {
"Routes": {
"legacy": {
"ClusterId": "legacy",
"Match": { "Path": "/legacy/{**catch-all}" }
}
},
"Clusters": {
"legacy": {
"Destinations": {
"app1": { "Address": "http://localhost:8080/" }
}
}
}
}
}
静的ファイルとフロントエンド
- Static Files:
wwwrootに配置しUseStaticFiles()を有効化。 - バンドル/圧縮:モダンなバンドラ(例:Vite/webpack)を採用し、キャッシュ制御ヘッダを付与。
- Anti-forgery:フォームは Tag Helper を使えば自動でトークンが入ります。
データ アクセス移行:既存 ADO.NET を活かすか、EF Core へ寄せるか
| 戦略 | 利点 | 注意点 |
|---|---|---|
| 既存 ADO.NET を再利用 | 移行コスト低い、動作リスク小 | 抽象化を追加しテストしやすくする |
| EF Core へ移行 | 保守性/生産性、LINQ、変更追跡 | SQL のチューニングと移行工数 |
// DbContext(例)
public class AppDbContext : DbContext
{
public DbSet<Customer> Customers => Set<Customer>();
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}
}
WCF を使っている場合の置き換え
- gRPC:強い型付けと高速通信が必要な場合に有効。
- REST(Minimal APIs / MVC):HTTP/JSON ベースでフロントとの親和性が高い。
// Minimal API(例)
var group = app.MapGroup("/api/orders");
group.MapGet("/{id:int}", async (int id, IOrderService s) => await s.GetAsync(id));
group.MapPost("/", async (CreateOrder c, IOrderService s) => await s.CreateAsync(c));
グローバリゼーションとローカライズ
using System.Globalization;
using Microsoft.AspNetCore.Localization;
builder.Services.AddLocalization(o => o.ResourcesPath = "Resources");
builder.Services.Configure(o =>
{
var cultures = new[] { new CultureInfo("ja-JP"), new CultureInfo("en-US") };
o.DefaultRequestCulture = new RequestCulture("ja-JP");
o.SupportedCultures = cultures;
o.SupportedUICultures = cultures;
});
app.UseRequestLocalization();
バックグラウンド処理(Timer/Global.asax 相当)
public sealed class ReportJob : BackgroundService
{
private readonly ILogger<ReportJob> _log;
public ReportJob(ILogger<ReportJob> log) => _log = log;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_log.LogInformation("Generate reports...");
// 仕事...
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
builder.Services.AddHostedService();
テストと品質ゲートの最小セット
- ユニットテスト:Presenter/サービス層を対象にロジックの回帰を防ぐ。
- 統合テスト:
WebApplicationFactory<T>でルーティング・フィルタ・認証を横断検証。 - UI テスト:重要導線のみ E2E(サインイン、注文、決済など)。
CI/CD とコンテナ:再現性の高いデプロイ
GitHub Actions(例)
name: build
on: [push]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- run: dotnet build --configuration Release
- run: dotnet test --configuration Release
- run: dotnet publish src/Web -c Release -o publish
- uses: actions/upload-artifact@v4
with:
name: site
path: publish
Dockerfile(IIS なしの自己ホスト)
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 src/Web -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "Web.dll"]
よくある落とし穴と回避策
- ViewState の移し替え過多:状態を持たせすぎると再発火問題や脆弱性の温床に。必要最小限に。
- サーバーコントロールの 1:1 置換:無理に完全再現せず、UI/UX を現代化。
- 巨大な一括リライト:リスクが高い。リバースプロキシで段階移行し、カナリアで検証。
- Membership の放置:早期に Identity へ切り替え、パスワード移行戦略を用意。
- Web.config の焼き直し:appsettings.json + Options に合わせて再設計。
- VB.NET UI コード:ASP.NET Core UI は C# 前提。ロジックも含め C# への変換を進める。
FAQ(現場でよく聞かれる質問)
Q. Upgrade Assistant でどこまで自動化できますか?
A. クラスライブラリやプロジェクト形式、参照更新などは支援されますが、.aspx/.ascx の UI は対象外です。UI は新規作成が前提です。
Q. 旧/新アプリのセッションや Cookie は共有できますか?
A. 共通ドメイン配下で Cookie を使い分ければ共存可能ですが、暗号方式や発行元が異なるため単純な共有は推奨されません。段階移行期間はシングルサインオンや再ログイン許容の設計が安全です。
Q. Ajax(UpdatePanel)をどう置き換えますか?
A. 部分ビュー + fetch(JSON)か、Blazor の部分更新に置き換えます。UI の非同期性をインタラクション単位に分解して設計し直すのがコツです。
Q. モノリスな Web フォームをどう切り出せば良いですか?
A. まずユースケース単位のアプリケーション サービス層を抽出し、それを新 UI から呼び出します。URL ルート単位で YARP/IIS で振り分け、重要導線から移行します。
Q. .NET 8 を選ぶメリットは?
A. 長期サポートの安定性、パフォーマンス、出力キャッシュ、Minimal APIs、Razor Pages/MVC/Blazor の成熟度など、実運用に必要な機能が揃っています。
最短ルートまとめ
- UI は手動移行一択(Web フォームは非サポート)。
- ロジックを先に取り出す(.NET Standard/.NET 8 ライブラリ化+テスト)。
- Razor Pages を基本線(CRUD を高速移植)。
- 認証は Identity、構成は appsettings.json、Global.asax は Middleware へ。
- リバースプロキシで共存し、カナリアで安全に切替。
- UpdatePanel・ViewState を卒業し、モデルバインディングと部分更新へ。
結論:ASPX の“自動変換”は存在しません。Razor Pages/MVC/Blazor へ UI を設計し直し、まずロジック層を .NET 8 に持ち上げてから段階的に画面を置き換える――これが最短でリスクの低い現実解です。

コメント