Start Debugging

.NET 11 で Blazor Server アプリを Blazor United (Blazor Web App) に移行する

スタンドアロンの Blazor Server アプリを .NET 11 の統合された Blazor Web App テンプレートへ移し、各ページを InteractiveServer のまま動作を変えずに維持するためのステップバイステップのチェックリストです。

スタンドアロンの Blazor Server アプリ (dotnet new blazorserver) があり、それを .NET 8 のプレビューサイクル中に「Blazor United」という愛称で呼ばれ Blazor Web App として出荷された統合テンプレートに移したい場合、移行はほとんど機械的で、小さなアプリなら通常半日、大きなアプリなら 1 ~ 3 日かかります。コンポーネントのコードは何も変える必要はありません。変わるのはホストです。_Host.cshtml がなくなり、ルーティングは Routes コンポーネントに移り、Program.csMapBlazorHubMapRazorComponents に置き換え、render mode を明示的に宣言します。各ページを @rendermode InteractiveServer のままにすれば、動作は .NET 7 時代の Blazor Server と同一のままです。唯一かみつくのはプリレンダリングの二重実行です。このガイドは .NET 11(執筆時点ではプレビュー、GA は 2026 年 11 月予定)と Microsoft.AspNetCore.Components 11.0.x パッケージセットを対象とします。

なぜスタンドアロンの Server テンプレートから離れるのか

スタンドアロンの blazorserver テンプレートは .NET 11 でも引き続き動作するため、これは強制的な移行ではありません。次のいずれかが本当に望む成果になったときに行い、それより前には行わないでください。

これらのいずれにも当てはまらない場合、既存のスタンドアロン Server アプリへの <TargetFramework>net11.0</TargetFramework> の変更は有効な代替手段であり、この移行ではありません。

何が壊れるか

領域変更重大度
ホストページPages/_Host.cshtml_Layout.cshtml がルートのホストコンポーネント App.razor に置き換わるhigh
ルーティング<Router>App.razor の外、新しい Routes.razor に移るhigh
Program.cs の起動AddServerSideBlazor() + MapBlazorHub() + MapFallbackToPage("/_Host")AddRazorComponents().AddInteractiveServerComponents() + MapRazorComponents<App>().AddInteractiveServerRenderMode() に置き換わるhigh
Render mode対話性はコンポーネントごとまたはグローバルにオプトイン。暗黙の「アプリ全体が対話的」はないhigh
プリレンダリングOnInitialized/OnInitializedAsync がデフォルトで 2 回実行される(プリレンダーパス + 対話パス)medium
クライアントスクリプト_framework/blazor.server.js_framework/blazor.web.js になるmedium
認証の配線CascadingAuthenticationState コンポーネントが DI の AddCascadingAuthenticationState() に置き換わるmedium
HttpContext アクセスHttpContext は静的 SSR 中のみ利用可能で、対話的コンポーネント内では使えないmedium
App.razor の意味App.razor はもはやルーターではなく、HTML ドキュメントのシェルであるlow

事前チェックリスト

移行手順

1. target framework とパッケージを引き上げる

.csproj を編集します。SDK は Microsoft.NET.Sdk.Web のままです。

<!-- .NET 11 -->
<PropertyGroup>
  <TargetFramework>net11.0</TargetFramework>
  <Nullable>enable</Nullable>
  <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

Microsoft.AspNetCore.Components.* のパッケージ参照を 11.0.x に引き上げます。明示的に参照されている場合は Microsoft.AspNetCore.Components.Web を削除します。これはウェブ SDK プロジェクトのフレームワーク参照の一部です。

確認: dotnet restore が完了し、dotnet build がパッケージ解決のエラーではなく、ホストページと Program.cs に関するエラー(この時点では想定どおり)でのみ失敗すること。

2. App.razor ホストコンポーネントを作成する

スタンドアロンの Server テンプレートでは、HTML ドキュメントは Pages/_Host.cshtmlPages/_Layout.cshtml にあります。そのマークアップを新しいルートの App.razor に移します(先に古い App.razor ルーターを削除するか、名前を変更します)。ルートをブートしていた <component> タグヘルパーは <Routes /> になります。

@* .NET 11 - App.razor is now the HTML document shell, not the router *@
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="YourApp.styles.css" />
    <HeadOutlet />
</head>
<body>
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>
</html>

見落としやすい変更が 2 つあります。スクリプトは blazor.web.js(blazor.server.js ではない)であること、そして <HeadOutlet /><Routes /> はコンポーネントなので、手順 6 で割り当てた render mode を受け継ぐことです。

確認: ファイルが Razor コンポーネントとしてコンパイルされること(@page ディレクティブなし、@model なし)。

3. ルーティングを Routes.razor に移す

プロジェクトのルートに Routes.razor を作成し、古い App.razor にあった <Router> ブロックを貼り付けます。

@* .NET 11 - Routes.razor holds the router that used to be in App.razor *@
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(Layout.MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

確認: dotnet buildRoutes 型の欠如についてもう文句を言わないこと。

4. _Imports.razor で render mode の短縮形を有効にする

完全修飾する代わりに InteractiveServer と書けるように、この行を _Imports.razor に追加します。

@* .NET 11 *@
@using static Microsoft.AspNetCore.Components.Web.RenderMode

確認: コンポーネント内の @rendermode InteractiveServer が using のエラーなしで解決されること。

5. Program.cs を書き直す

Blazor Server の登録とエンドポイントを Razor Components 相当に置き換えます。

// .NET 11, C# 14 - Blazor Web App startup
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

// keep your existing app services here (DbContext, HttpClient, etc.)

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

AddServerSideBlazor()app.MapBlazorHub()app.MapFallbackToPage("/_Host") を削除します。app.UseAntiforgery() の呼び出しは新しく、必須です。Web App テンプレートはデフォルトで antiforgery ミドルウェアを有効にしており、これがないとフォームの POST が失敗します。

確認: dotnet build がエラーゼロで完了すること。

6. アプリを対話的にする(グローバル render mode)

最もリスクの低い移行は、アプリ全体を InteractiveServer にして、古い動作を正確に再現することです。App.razor の中の <Routes /><HeadOutlet /> に render mode を設定します。

@* .NET 11 - global InteractiveServer, matches standalone Blazor Server behaviour *@
<HeadOutlet @rendermode="InteractiveServer" />
...
<Routes @rendermode="InteractiveServer" />

確認: dotnet run を実行し、アプリを開き、対話的コンポーネント(ボタン、@onclickEditForm の送信)が動作し、ブラウザが /_blazor への WebSocket を開いたまま保持していることを確認します。これが動いたら、あとで個々のページを @rendermode StaticServer に下げたり、アイランドを WebAssembly に上げたりできますが、それは移行後の作業です。

7. プリレンダリングの二重実行に対処する

これは人々を驚かせる唯一の動作変更です。対話的な render mode では、Blazor はまず静的 HTML をプリレンダーし、その後ライブの circuit 上で再びレンダーするため、OnInitializedOnInitializedAsync が 2 回実行されます。古いスタンドアロン Server のデフォルト(render-mode="ServerPrerendered")も同じ性質を持っていましたが、多くのアプリは render-mode="Server" を使っていて、これに気づくことはありませんでした。

選択肢は 3 つあります。.NET 11 で最もきれいなのは宣言的な [PersistentState] 属性(.NET 10 で追加)です。プリレンダー中に一度フェッチし、HTML にシリアライズして、対話パスで復元します。

// .NET 11 - fetch once, survive the prerender-to-interactive handoff
public partial class Dashboard : ComponentBase
{
    [PersistentState]
    public List<Order>? Orders { get; set; }

    [Inject] public required IOrderService OrderService { get; init; }

    protected override async Task OnInitializedAsync()
    {
        // Orders is non-null on the interactive pass: state was restored,
        // so the service is not hit a second time.
        Orders ??= await OrderService.GetRecentAsync();
    }
}

プリレンダー中にまったくフェッチしたくない場合は、その境界でプリレンダリングを無効にします。

@* .NET 11 - skip the prerender pass entirely for this component tree *@
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />

確認: 副作用のある各 OnInitializedAsync にブレークポイントまたはログ行を置き、実際のナビゲーションごとに 1 回だけ実行され、2 回ではないことを確認します。

8. 認証と認可を再配線する

アプリがルーターを包む <CascadingAuthenticationState> を使っていた場合、そのコンポーネントを削除して代わりに DI に登録し、Routes.razorRouteViewAuthorizeRouteView に置き換えます。

// .NET 11 - Program.cs
builder.Services.AddCascadingAuthenticationState();
@* .NET 11 - Routes.razor, authorized routing *@
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />

確認: サインアウト状態で [Authorize] ページにアクセスしてリダイレクトされることを確認し、その後サインインしてアクセスできることを確認します。

9. 不要になったホストファイルを削除する

Pages/_Host.cshtmlPages/_Layout.cshtml、および _Host を提供するためだけに存在していた app.MapRazorPages() の呼び出しを削除します。Razor Pages を他に何も使っていない場合は、Program.cs から AddRazorPages() を削除します。

確認: dotnet build がクリーンで、アプリがすべてのルートを引き続き提供すること。

検証: 移行後のスモークテスト

マージする前に、これらすべてを実行します。

ロールバック計画

この移行が元に戻せるのは、事前チェックの手順でブランチを保持していた場合のみです。_Host.cshtml を削除して Program.cs を書き直すと、スタンドアロン Server モデルへ戻すその場での切り替えはありません。前進方向に編集するのではなく、移行前のコミットをチェックアウトしてロールバックします。変更は構造的なものでデータ移行ではないため、データベースやストレージで元に戻すものはありません。ブランチを作り、作業を行い、スモークテストで検証し、グリーンになったときだけマージします。

ぶつかった落とし穴

ここでの行き先はほぼ常に「各ページを InteractiveServer に、プリレンダリングは処理済み」です。これはユーザーが見られるものを何も変えない移行です。Static Server ページ、WebAssembly アイランド、Auto コンポーネントを追加することは、そのあとで、1 コンポーネントずつ、さらなる構造的な揺さぶりなしに回収する見返りです。

関連

出典

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る