ASP.NET Webフォーム(.aspx)から .NET 8(ASP.NET Core)への移行完全ガイド|Razor Pages・MVC・Blazor・段階移行の実務

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 Pages1 ページ = 1 ページモデル。コードビハインドの移植が容易。CRUD 中心、画面数が多い、段階移行でスピード重視
MVCController と View の分離が明確。テスト容易。複雑なルーティング、Web API との共存や拡張性重視
Blazor Server / WebAssemblyC# 中心に SPA を実現。Presenter ロジックの再利用性高い。リッチな対話 UI、JS 依存を減らしたい、リアルタイム性

実務で使える「段階移行」ロードマップ(安全に止めずに進める)

  1. 整理・設計:画面一覧、依存関係、データソース、認証方式、URL を棚卸しし、優先度と投入順を決定。
  2. ロジック分離:Presenter とドメインロジックを .NET Standard 2.0/2.1 あるいは .NET 8 のクラスライブラリへ抽出。単体テストを追加。
  3. 非 UI ライブラリの先行移行:Upgrade Assistant 等で共通ライブラリを .NET 8 化し、ビルド & テストを安定化。
  4. UI の書き換え:Razor Pages/MVC/Blazor へ順次移植。ViewState/ポストバックはモデルバインディングへ。
  5. インフラ更新:認証(ASP.NET Identity)、構成(appsettings.json)、DI、ログ、キャッシュ、セッション、ミドルウェアに置換。
  6. 併用・カナリア:IIS や YARP でリバースプロキシを構成し、旧/新サイトをパス単位で共存。
  7. 切替・縮退:アクセスを段階的に新サイトへ寄せ、エラーレート・パフォーマンス監視を通過後に旧画面を縮退。

概算の目安(粗い見積り式)

画面タイプ移行難易度標準工数の目安備考
単純 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) + CodeBehindRazor Page + PageModelイベント→ハンドラ(OnGet/OnPost)に集約
PresenterDI された Application Serviceインターフェース経由で注入
ViewState / PostBackModelBinding / TempData / Hidden必要最小限の状態のみ保持
Server Controls(GridView 等)Tag Helper / 部分ビュー / コンポーネント描画とロジックを分離
UserControl(.ascx)Partial View / ViewComponent / Blazor Component再利用単位を明確化
MasterPage_Layout.cshtmlセクションで拡張
Global.asax / HttpModuleProgram.cs / Middleware順序と委譲が重要
Membership ProviderASP.NET Identity(Cookie/JWT)段階移行なら一時ブリッジも可
Web.configappsettings.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&lt;CustomerDto&gt;
@if (TempData["Flash"] is string f) { &lt;div class="alert"&gt;@f&lt;/div&gt; }
&lt;form asp-action="Create" method="post"&gt;
  &lt;input name="Name" /&gt;
  &lt;button type="submit"&gt;追加&lt;/button&gt;
&lt;/form&gt;
&lt;ul&gt;@foreach (var c in Model) { &lt;li&gt;@c.Name&lt;/li&gt; }&lt;/ul&gt;

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/RepeaterHTML テーブル + ループ、コンポーネントソート/ページングはサーバー側 API 化
ValidatorsDataAnnotations + クライアント検証サーバー側での再検証は必須

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

新規は ASP.NET Identity(Cookie/JWT)を採用します。既存ユーザーを段階的に移すなら「ログイン時移行」戦略が安全です。

  1. Identity スキーマで新テーブルを用意。
  2. ログイン要求が来たら旧ユーザー テーブルで検証。
  3. 成功したら Identity にユーザーを作成し、以後は新側で認証。
// Cookie 認証(概要)
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(o =&gt; {
        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&lt;FeatureFlags&gt;(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&lt;Customer&gt; Customers =&gt; Set&lt;Customer&gt;();
  public AppDbContext(DbContextOptions&lt;AppDbContext&gt; 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) =&gt; await s.GetAsync(id));
group.MapPost("/", async (CreateOrder c, IOrderService s) =&gt; 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 に持ち上げてから段階的に画面を置き換える――これが最短でリスクの低い現実解です。

この記事を書いた人

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

コメント

コメントする

目次