Blazor Serverを.NET 8でWASM Autoレンダーモードへ段階移行する完全ガイド

.NET 8 で Blazor が「Blazor Web App」へ生まれ変わり、Server・WASM・Auto などのレンダーモードをページ単位で選べるようになりました。しかし、すでに Blazor Server で動いている業務アプリに対して、最小限の変更で WASM(Auto)を段階導入する方法は意外と情報が少なく、悩んでいる方も多いはずです。本記事では、現実的な移行戦略と具体的な実装パターンを、サンプルコード付きで詳しく解説します。

目次

Blazor Server から WASM(Auto)に段階移行したい理由

まず、なぜ既存の Blazor Server アプリに対して、わざわざ WASM/Auto モードを追加したくなるのかを整理しておきます。

  • 画面のレスポンスを上げたい(ラウンドトリップを減らしたい)
  • SignalR 接続が切れても、ある程度はクライアント側で動いてほしい
  • 一部のページだけでもオフライン寄りの挙動にしたい
  • 将来的には「ほぼ WASM」だが、すぐには全部書き換えられない

.NET 8 以降の Blazor Web App では、ページ/コンポーネント単位で以下のレンダーモードを選べます。

レンダーモード実行場所特徴主な用途
InteractiveServerサーバーSignalR 経由で UI を双方向更新。既存 Blazor Server とほぼ同じ。既存アプリ・重い処理やサーバーリソースが前提の画面
InteractiveWebAssemblyブラウザー(WASM).NET ランタイムごとダウンロード。サーバーは API を中心に。レスポンス重視、オフライン寄りの画面
InteractiveAutoSSR+WASM初回はサーバー側で SSR、起動後は WASM に引き継ぎ。SEO と体感速度の両立が必要な画面
Staticサーバーのみインタラクティブ不要な静的ページ。規約ページ、単純なお知らせなど

この記事のゴールは、Blazor Server で構築済みのアプリに対して、InteractiveAuto をページ単位で導入できる状態にすることです。

戦略の選択肢:新規テンプレート移植 vs 既存プロジェクト拡張

.NET 8 の世界で Auto モードをフルに活用するには、大きく次の 2 つの戦略があります。

1. 新規 Blazor Web App テンプレートから移植する方法(推奨)

一番安全でトラブルが少ないのは、「Blazor Web App(Auto/ページ単位・個別認証)」テンプレートを新規作成し、既存コードを移植する方法です。

  • 新規テンプレで InteractiveServer を基本として動作確認
  • 既存のコンポーネントを InteractiveServer として移植
  • 問題ないページから順次 @rendermode InteractiveAuto を付与

この方法のメリットは、「テンプレートが .NET 8 標準のベストプラクティスを既に組み込んでいる」ことです。AppHost の構成、レンダーモード、DI、Static Web Assets などの細かい罠を回避しやすくなります。

2. 既存 Blazor Server プロジェクトを拡張する方法(最小変更)

とはいえ、「既に運用中のプロジェクトに、新しく Blazor Web App を立てて移植するのは社内調整が大変…」というケースも多いはずです。その場合は、既存プロジェクトを以下のように段階的に拡張していきます。

観点新規テンプレ移植既存プロジェクト拡張
影響範囲中〜大(プロジェクト構成が変わる)小〜中(段階的に対応可能)
学習コスト新テンプレ構成を理解する必要あり既存構成の延長で対応しやすい
トラブルの起きにくさ高(標準構成に沿える)中(構成ミスや DI 漏れに注意)
リスク許容度テスト環境が用意できるなら最有力既存サーバーをなるべく触りたくない場合に有効

本記事では特に需要の多い、「既存 Blazor Server プロジェクトを最小変更で Auto 対応させる」パターンを中心に解説します。

既存 Blazor Server を最小変更で Auto 対応する全体像

大まかな流れは次のとおりです。

  1. ソリューションに Client(WASM)/Shared プロジェクトを追加
  2. Server 側の Program.cs を更新し、WebAssembly コンポーネントを有効化
  3. 小さなページから @rendermode InteractiveAuto を試す
  4. DB やファイル I/O をサービス I/F 経由に整理し、API で公開
  5. 認証・認可を API/WASM 対応に拡張
  6. SSR と WASM 間の状態引き継ぎで二重フェッチを防ぐ
  7. 重いページは当面 InteractiveServer のまま運用し、段階的に Auto 化

このステップを意識すると、「いきなり全部 WASM」にせず、リスクを抑えながら徐々にモダナイズできます。

プロジェクト構成:Server / Client / Shared に分割する

まずはソリューションを、Blazor Web App に近い 3 層構成に寄せます。

プロジェクト役割主な中身
MyApp.Serverホスト・API・DB アクセスProgram.cs、EF Core、リポジトリ、API エンドポイント、認証設定
MyApp.ClientWASM 実行用コンポーネントAuto/WebAssembly モードにするコンポーネント、WASM 用 DI 設定
MyApp.SharedUI & 契約共有Razor コンポーネント、DTO、ViewModel、サービス I/F、バリデーション

ポイントは、UI から「サーバー専用 API」への直アクセスを排除し、Shared 層に置いたインターフェイス経由に統一することです。例えば注文画面なら、以下のような I/F を Shared に切り出します。

public interface IOrderService
{
    Task<OrderDto> GetAsync(int id);
    Task SaveAsync(OrderDto order);
}

Server 側は従来どおり EF Core やリポジトリを使って実装し、WASM 側は HttpClient で API を叩く実装にします。UI コンポーネントは IOrderService しか知らないため、レンダーモードを Server → Auto/WASM に切り替えても、呼び出しコードは変えずに済みます。

Server 側 Program.cs の更新:WASM コンポーネントを有効にする

次に、Server プロジェクトの Program.cs を更新し、Razor コンポーネントのレンダーモードとして Server と WASM の両方を使えるようにします。

var builder = WebApplication.CreateBuilder(args);

// Razor コンポーネントの登録
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddInteractiveWebAssemblyComponents();

// アプリ固有サービス(Server 実装)
builder.Services.AddScoped<IOrderService, OrderServiceServer>();

// API ドキュメント等が必要であれば
builder.Services.AddEndpointsApiExplorer();
// Swagger を使う場合は AddSwaggerGen など

// 認証・認可(例:Cookie 認証)
builder.Services
    .AddAuthentication("Cookies")
    .AddCookie("Cookies");

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.UseStaticFiles();

// Razor コンポーネントのマッピング
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .AddInteractiveWebAssemblyRenderMode();

// API 例:/api/orders 以下にエンドポイントをまとめる
app.MapGroup("/api/orders")
   .RequireAuthorization()
   .MapOrdersEndpoints();

app.Run();

ここでの重要ポイントは次のとおりです。

  • AddInteractiveServerComponents() と AddInteractiveWebAssemblyComponents() の両方を登録する
  • MapRazorComponents<App>() に対して、Server/WASM 両方のレンダーモードを追加する
  • API は app.MapGroup("/api")... のように URL を整理しておくと、WASM 側からの呼び出しが分かりやすい

最初の Auto ページを作ってみる

設定ができたら、いきなり全ページを Auto にするのではなく、まずは 1 ページだけ Auto を試すのが安全です。

1. 既存ページは InteractiveServer のまま動かす

Blazor Server 時代のページは、まずは以下のように @rendermode InteractiveServer を明示して、従来通り動くことを確認します。

@page "/orders/{id:int}"
@rendermode InteractiveServer
@inject IOrderService Orders

&lt;h2&gt;注文詳細 (Server)&lt;/h2&gt;

ここまででエラーが出る場合は、Server 側の DI や認証設定を先に整える必要があります。

2. 小さなページから InteractiveAuto に切り替える

次に、負荷が低く依存関係の少ないページ(純 UI に近い画面)から、Auto モードを試します。

@page "/orders/{id:int}"
@rendermode InteractiveAuto
@inject IOrderService Orders

&lt;h2&gt;注文詳細 (Auto)&lt;/h2&gt;

@if (Order is null)
{
    &lt;p&gt;読み込み中...&lt;/p&gt;
}
else
{
    &lt;p&gt;注文番号: @Order.Id&lt;/p&gt;
}

この時、WASM に乗せたいコンポーネントは Client プロジェクト側に配置するようにしておくと、後々構成が整理しやすくなります。Auto モードでは、初回は SSR(サーバー側レンダリング)で HTML が返され、その後に WASM が起動して UI の制御を引き継ぎます。

データアクセスを二層化する:サービス I/F + Web API

段階移行の最大のポイントは、「コンポーネントから DB やファイルへ直接アクセスしているコードを、すべてサービス I/F 経由に隠蔽する」ことです。

1. I/F を Shared に定義する

public interface IOrderService
{
    Task&lt;OrderDto&gt; GetAsync(int id);
    Task SaveAsync(OrderDto order);
}

ここでの注意点は以下です。

  • EF Core のエンティティ(DbContext のモデル)をそのまま UI に渡さない
  • UI に渡すのは DTO や ViewModel に変換した上でにする
  • 更新パターンが複雑な場合は、Command オブジェクトを別途定義する

2. Server 実装:既存の DB アクセスをラップする

public class OrderServiceServer : IOrderService
{
    private readonly AppDbContext _db;

    public OrderServiceServer(AppDbContext db)
    {
        _db = db;
    }

    public async Task&lt;OrderDto&gt; GetAsync(int id)
    {
        var entity = await _db.Orders.FindAsync(id)
                     ?? throw new KeyNotFoundException();

        return new OrderDto
        {
            Id = entity.Id,
            CustomerName = entity.CustomerName,
            Total = entity.Total
        };
    }

    public async Task SaveAsync(OrderDto order)
    {
        var entity = await _db.Orders.FindAsync(order.Id)
                     ?? new Order();

        entity.CustomerName = order.CustomerName;
        entity.Total = order.Total;

        _db.Update(entity);
        await _db.SaveChangesAsync();
    }
}

Server 側 DI では、先ほどのように OrderServiceServer を登録します。

builder.Services.AddScoped&lt;IOrderService, OrderServiceServer&gt;();

3. WASM 実装:HttpClient で API を呼び出す

Auto/WASM で動作させるためには、同じ I/F を満たす WASM 実装が必要です。こちらは HttpClient を使って Server 側 API を呼び出します。

public class OrderServiceHttp : IOrderService
{
    private readonly HttpClient _http;

    public OrderServiceHttp(HttpClient http)
    {
        _http = http;
    }

    public async Task&lt;OrderDto&gt; GetAsync(int id)
    {
        var dto = await _http.GetFromJsonAsync&lt;OrderDto&gt;(
            $"/api/orders/{id}");

        return dto ?? throw new InvalidOperationException("Order not found.");
    }

    public async Task SaveAsync(OrderDto order)
    {
        var response = await _http.PutAsJsonAsync(
            $"/api/orders/{order.Id}", order);

        response.EnsureSuccessStatusCode();
    }
}

Client プロジェクト(WASM 側)の Program.cs 相当では、次のように DI を設定します。

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.Services.AddScoped(sp =&gt;
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

builder.Services.AddScoped&lt;IOrderService, OrderServiceHttp&gt;();

await builder.Build().RunAsync();

こうしておけば、UI から見ると常に IOrderService しか見えないので、レンダーモードに応じて Server 側/WASM 側の具体実装が差し替えられます。

認証・認可:Server と WASM で整合をとる

Blazor Server 時代は「すべてサーバー上で動く」ため、HttpContext.User や [Authorize] に頼ってもあまり問題になりませんでした。しかし WASM が絡むと、次のポイントを設計し直す必要があります。

  • WASM から API を呼ぶ際に、どうやって認証情報を渡すか
  • クライアント側で見せてはいけない情報をどう守るか

パターン 1:Cookie 認証で API を呼ぶ

既に Cookie 認証(フォーム認証など)で統一しているなら、そのまま Cookie を使って API を呼ぶのが一番自然です。

  • SameSite=Lax or None に設定(HTTPS 前提)
  • API 側に [Authorize] を付与
  • CSRF 対策として Anti-forgery トークンやカスタムヘッダーを設計

Chrome などの最新ブラウザーでは SameSite の挙動が厳しくなっているため、ログイン画面と API のドメイン/サブドメイン構成も含めて確認しておくと安心です。

パターン 2:JWT(Bearer)トークン方式

モバイルアプリや外部クライアントとの連携も視野に入るなら、JWT を使った Bearer 認証に切り替えるのも有力な選択肢です。

  • ログイン時にアクセストークン+リフレッシュトークンを発行
  • WASM からは Authorization ヘッダーでトークンを送信
  • トークンの有効期限/ローテーション戦略を明確にする

いずれの方式でも、クライアント側の認証情報は「表示用」であり、権限チェックの最終判断は API 側で行うというスタンスを徹底しましょう。

AuthenticationStateProvider の実装

WASM 側でユーザー情報を扱うには、AuthenticationStateProvider をカスタム実装し、API から現在のユーザー情報を取得するパターンが一般的です。

  • WASM 起動時に /api/auth/me のようなエンドポイントを叩き、ClaimsPrincipal を構築
  • 結果をメモリキャッシュし、UI の認可表示に利用

このあたりはアプリごとの要件差が大きいので、「まずは Cookie 認証で [Authorize] 付き API を叩ける状態」を小さく作り、そこから本番レベルに育てていくのがおすすめです。

SSR から WASM への状態引き継ぎ:二重ロードを避ける

Auto モードでは、初回表示が SSR で行われ、その後 WASM が起動して状態を引き継ぎます。このとき、SSR で読み込んだデータを、WASM 起動後も再利用しないと API 呼び出しが二重になるという問題が起きがちです。

これを防ぐために使えるのが PersistingComponentState などの状態永続化 API です。SSR 側で取得したデータをシリアライズし、WASM 側で取り出してから、必要な場合だけ API を呼ぶようにします。

@inject PersistentComponentState ApplicationState
@inject IOrderService Orders

OrderDto? Order;

protected override async Task OnInitializedAsync()
{
    if (!ApplicationState.TryTakeFromJson&lt;OrderDto&gt;("order", out var order))
    {
        // SSR ではここで取得して永続化、WASM 起動後はキャッシュを使用
        Order = await Orders.GetAsync(/* id */ 1);
        ApplicationState.PersistAsJson("order", Order);
    }
    else
    {
        Order = order;
    }
}

実際には SSR フェーズと WASM フェーズで処理を分けるため、もう少しコードが増えますが、考え方としては「SSR で一回取ったデータは、できる限り再利用する」ことを徹底します。

フォールバック運用:重いページはあえて Server のまま残す

全ページを Auto/WASM にしようとすると、以下のような画面で苦労するケースが多いです。

  • 巨大なグリッドや帳票など、データ量が桁違いに多い画面
  • サーバー上のファイルシステムや OS 機能と強く結びついている画面
  • 外部プロセス起動や COM コンポーネントなど、ブラウザーからはどうしても触れない処理

こういった画面は、当面は @rendermode InteractiveServer のままにしておく方が現実的です。アプリ全体を瞬時に WASM 化しようとせず、次のような優先度で移行していきましょう。

優先度対象コンポーネント推奨アクション
高純 UI(データ呼び出しなし)即 Auto に切り替え
中読み取り専用ページサービス I/F 化 → API 化 → Auto
中軽量な更新系ページAPI にバリデーション&権限チェックを実装してから Auto
低OS/ファイル/プロセス依存の強いページ最後まで Server に残すか、別システムとして切り出す

よくある落とし穴と対策

Blazor Server アプリをそのまま Auto/WASM に載せ替えようとすると、次のようなエラーや不具合に悩まされがちです。

DbContext をコンポーネントへ直接注入している

@inject AppDbContext Db のように、コンポーネントから直接 DbContext を触っている場合、そのコンポーネントは Server 専用になってしまいます。WASM では DbContext 自体が動かないため、必ずサービスでラップし、Shared からは I/F だけを見るようにしましょう。

IHttpContextAccessor などサーバー専用 API に依存している

IHttpContextAccessor、HttpContext、WindowsIdentity などに依存しているコードも、そのままでは WASM 化できません。これらは API 層で使用し、クライアント側には必要な情報だけ DTO として渡す設計に改めます。

SSR 後の二重ロード

Auto モードで SSR → WASM に切り替わる際、同じ API を 2 回呼んでしまうケースは非常によくあります。先述の PersistingComponentState や、単純な場合は「初回読み込み済みフラグ」を利用して、二重読み込みを防ぎましょう。

DI 登録漏れ(Server にはあるが Client にはない)

Shared に置いた I/F に対して、Server 側だけ実装を登録し、Client 側 DI を忘れているケースも多発します。「Shared の I/F 一つにつき、Server 実装と WASM 実装の両方があるか」をチェックリスト化しておくと安心です。

CSRF・認証設定の不備

Cookie 認証で API を呼ぶ場合、CSRF 対策を考慮しないと、思わぬ脆弱性を抱えることになります。Anti-forgery トークン、SameSite 設定、CORS 設定などを整理し、「ブラウザーからどのオリジンで API を叩くのか」を明確にしましょう。

最小サンプル構成のおさらい

最後に、本記事で紹介した最小構成を、要点だけ抜き出してまとめておきます。

Server: Program.cs(要旨)

var builder = WebApplication.CreateBuilder(args);

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

// アプリ固有サービス(Server 実装)
builder.Services.AddScoped&lt;IOrderService, OrderServiceServer&gt;();

builder.Services.AddAuthentication("Cookies")
    .AddCookie("Cookies");

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.UseStaticFiles();

app.MapRazorComponents&lt;App&gt;()
   .AddInteractiveServerRenderMode()
   .AddInteractiveWebAssemblyRenderMode();

// API 例
app.MapGroup("/api/orders")
   .RequireAuthorization()
   .MapOrdersEndpoints();

app.Run();

注文ページ(Razor)

@page "/orders/{id:int}"
@rendermode InteractiveAuto
@inject IOrderService Orders

&lt;h2&gt;注文詳細&lt;/h2&gt;

@if (Order is null)
{
    &lt;p&gt;読み込み中...&lt;/p&gt;
}
else
{
    &lt;p&gt;注文番号: @Order.Id&lt;/p&gt;
}

Client(WASM)側の DI(要旨)

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.Services.AddScoped(sp =&gt;
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

builder.Services.AddScoped&lt;IOrderService, OrderServiceHttp&gt;();

await builder.Build().RunAsync();

この最小構成でも、

  • 同じ Razor コンポーネントが Server/WASM 両方で動く
  • ページ単位で InteractiveServer/InteractiveAuto を切り替えられる
  • サービス I/F をはさんでいるため、実装差し替えが容易

といったメリットを得ることができます。

まとめ:Auto モードへの段階移行を現実的に進めるコツ

最後に、Blazor Server から WASM(Auto)への段階移行を成功させるためのポイントを、改めて整理します。

  • 最初から全部を WASM にしない:純 UI → 読み取り専用 → 更新系 → OS 依存の順で少しずつ移行する
  • サービス I/F + API 化を徹底する:コンポーネントから DB やファイルに直アクセスしない
  • 認証・認可を先に整える:Cookie か JWT か方針を決め、API 側で権限チェックを行う
  • SSR と状態引き継ぎで体感速度を上げる:PersistingComponentState などで二重フェッチを防ぐ
  • 落とし穴チェックリストを持つ:DbContext 直注入、HttpContext 依存、DI 漏れ、CSRF 設定など

.NET 8 の Blazor Web App とレンダーモードの仕組みを踏まえつつ、既存 Blazor Server アプリを「壊さずに」育てていくには、構成・サービス I/F・認証・状態管理の 4 点を先に固めることが近道です。本記事の内容をベースに、自分のプロジェクトに合った粒度で少しずつ Auto モードを導入していけば、リスクを抑えながら最新の Blazor 体験に近づけていけるはずです。

この記事を書いた人

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

コメント

コメントする

目次