.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 を中心に。 | レスポンス重視、オフライン寄りの画面 |
| InteractiveAuto | SSR+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 対応する全体像
大まかな流れは次のとおりです。
- ソリューションに Client(WASM)/Shared プロジェクトを追加
- Server 側の Program.cs を更新し、WebAssembly コンポーネントを有効化
- 小さなページから
@rendermode InteractiveAutoを試す - DB やファイル I/O をサービス I/F 経由に整理し、API で公開
- 認証・認可を API/WASM 対応に拡張
- SSR と WASM 間の状態引き継ぎで二重フェッチを防ぐ
- 重いページは当面
InteractiveServerのまま運用し、段階的に Auto 化
このステップを意識すると、「いきなり全部 WASM」にせず、リスクを抑えながら徐々にモダナイズできます。
プロジェクト構成:Server / Client / Shared に分割する
まずはソリューションを、Blazor Web App に近い 3 層構成に寄せます。
| プロジェクト | 役割 | 主な中身 |
|---|---|---|
| MyApp.Server | ホスト・API・DB アクセス | Program.cs、EF Core、リポジトリ、API エンドポイント、認証設定 |
| MyApp.Client | WASM 実行用コンポーネント | Auto/WebAssembly モードにするコンポーネント、WASM 用 DI 設定 |
| MyApp.Shared | UI & 契約共有 | 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
<h2>注文詳細 (Server)</h2>
ここまででエラーが出る場合は、Server 側の DI や認証設定を先に整える必要があります。
2. 小さなページから InteractiveAuto に切り替える
次に、負荷が低く依存関係の少ないページ(純 UI に近い画面)から、Auto モードを試します。
@page "/orders/{id:int}"
@rendermode InteractiveAuto
@inject IOrderService Orders
<h2>注文詳細 (Auto)</h2>
@if (Order is null)
{
<p>読み込み中...</p>
}
else
{
<p>注文番号: @Order.Id</p>
}
この時、WASM に乗せたいコンポーネントは Client プロジェクト側に配置するようにしておくと、後々構成が整理しやすくなります。Auto モードでは、初回は SSR(サーバー側レンダリング)で HTML が返され、その後に WASM が起動して UI の制御を引き継ぎます。
データアクセスを二層化する:サービス I/F + Web API
段階移行の最大のポイントは、「コンポーネントから DB やファイルへ直接アクセスしているコードを、すべてサービス I/F 経由に隠蔽する」ことです。
1. I/F を Shared に定義する
public interface IOrderService
{
Task<OrderDto> 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<OrderDto> 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<IOrderService, OrderServiceServer>();
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<OrderDto> GetAsync(int id)
{
var dto = await _http.GetFromJsonAsync<OrderDto>(
$"/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 =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<IOrderService, OrderServiceHttp>();
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<OrderDto>("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<IOrderService, OrderServiceServer>();
builder.Services.AddAuthentication("Cookies")
.AddCookie("Cookies");
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode();
// API 例
app.MapGroup("/api/orders")
.RequireAuthorization()
.MapOrdersEndpoints();
app.Run();
注文ページ(Razor)
@page "/orders/{id:int}"
@rendermode InteractiveAuto
@inject IOrderService Orders
<h2>注文詳細</h2>
@if (Order is null)
{
<p>読み込み中...</p>
}
else
{
<p>注文番号: @Order.Id</p>
}
Client(WASM)側の DI(要旨)
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<IOrderService, OrderServiceHttp>();
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 体験に近づけていけるはずです。

コメント