Blazor WebAssembly と Blazor Server で実現する .NET 9 Blazor Web アプリ設計と Cookie 共有パターン

Blazor WebAssembly と Blazor Server を組み合わせた構成は強力ですが、「Cookie をどう扱うか」「Server プロジェクトを直接参照できない」など、設計を間違えるとすぐに行き詰まります。本記事では .NET 9 の Blazor Web アプリを前提に、プロジェクト構成と Cookie/ユーザーデータの受け渡し方法を、実務レベルで整理して解説します。

目次

Blazor WebAssembly から Blazor Server プロジェクトを直接参照できない理由

まず最初のポイントは、「Blazor WebAssembly(以下 WASM)プロジェクトから、Blazor Server プロジェクトを プロジェクト参照で直接参照することはできない」という事実です。 .NET 5 の頃からこの制限は変わっておらず、.NET 9 の Blazor Web アプリでも同様に Visual Studio が参照追加を拒否します。

実行環境がまったく違うため

一見すると「同じ C# だから参照できてもよさそう」と思えますが、Blazor Server と Blazor WebAssembly はそもそも動いている場所とランタイムが違います。

項目Blazor ServerBlazor WebAssembly
実行場所サーバー(ASP.NET Core ホスト)ブラウザ内(WebAssembly サンドボックス)
ランタイムフル .NET ランタイム.NET for WebAssembly(制限付き)
ネットワークアクセスサーバー側から自由に可能ブラウザからの HTTP 通信に限定
ファイル/DB への直接アクセス許可(アプリ側で制御)原則不可(Web API 経由が前提)
プロジェクト参照同じ .NET ランタイム同士なら可能Server プロジェクトを直接参照するのは不可

WASM から Server プロジェクトを直接参照できてしまうと、ブラウザ上のコードから SqlConnection やファイル IO など、ブラウザには存在しない API を呼び出せてしまう設計になります。これはビルドまたは実行時に確実に破綻します。そのため、Visual Studio 側でそもそも「プロジェクト参照を追加させない」ように制限されているわけです。

.NET 5 時代からの制限は .NET 9 でも継続

「.NET 9 の Blazor Web アプリなら、何か新機能で解決しているのでは?」と期待したくなりますが、ここはアーキテクチャ上の根本的な制約なので方針は変わっていません。 .NET 9 の Blazor Web アプリ テンプレートでは、Server と WASM が一体化されたプロジェクト構成になりましたが、それでも

  • Server コードをブラウザに持ち込むこと
  • ブラウザ側から Server プロジェクトを直接参照すること

はできません。あくまで「サーバー側がクライアント DLL を読み込む」方向であり、その逆は許されていない点が重要です。

やってはいけない設計の例

誤りやすいアンチパターンをいくつか挙げておきます。

  • WASM から直接 DB にアクセスしようとする
    例:DbContext を WASM 側に持ち込み、サーバーの DB に直アクセスする構想。 → ブラウザから直接 DB にはつながらないため、設計として破綻しています。
  • Server プロジェクトのサービス実装を WASM から呼び出そうとする
    例:Server 側の OrderService を WASM に参照させたい。 → そもそも参照を張れないので実現不可。共通インターフェース化+Web API 経由が必要です。
  • 「Visual Studio が拒否するだけで、何か裏技があるのでは?」と考える
    → もし無理やり DLL をコピーしても、ランタイムや API の違いで実行時エラーが出るだけです。

では、実際に Blazor WebAssembly と Blazor Server 間でロジックやデータを共有したい場合、どう設計するのが正しいのでしょうか。ここからは、現実的な代替案を詳しく見ていきます。

代替案 1: Blazor Web App の新構成を活かす

.NET 8 以降の「Blazor Web App」テンプレートでは、Server と WASM を単一プロジェクト内で扱う構成が採用されています。 サーバー側がクライアント DLL を読み込み、プリレンダリングと組み合わせて Cookie や永続ステートを埋め込むことで、WASM 側へ安全に情報を渡すことができます。

Blazor Web App 構成のざっくり比較

項目従来: Server + WASM 別プロジェクトBlazor Web App (.NET 8+)
プロジェクト数Server / Client の 2 プロジェクト基本 1 プロジェクト(内部で分離ビルド)
クライアント DLL の扱いClient プロジェクトとして個別に存在<Client> フォルダー以下を WASM としてビルド
サーバーからの利用Server から Client を参照してコンポーネントを使うサーバーがクライアント DLL を読み込み、プリレンダリング
Cookie を利用したステート共有自前実装が多くなりがちプリレンダ+永続ステートを組み合わせやすい

この構成では、「サーバーが主」で、「WASM はあくまでブラウザ側で動くフロントエンド」という位置づけがより明確になります。 Cookie を読み取るのはサーバーの役割であり、その結果を WASM にどう受け渡すかを、プリレンダリングと永続ステートで設計していくイメージです。

Server prerender で Cookie を読み取り、永続ステートとして埋め込む

大まかな流れは次のようになります。

  1. ブラウザからの初回リクエストに Cookie が付与されてサーバーに到達する。
  2. サーバー側(Blazor Server 部分)が Cookie を読み取り、AppData などのモデルにまとめる。
  3. プリレンダリング中にその AppData を永続ステート(例: Persistent Component State)として HTML 内に埋め込む。
  4. WASM が起動するとき、JS Interop を通じて埋め込まれたステートを読み取り、クライアント側の初期状態として利用する。

簡易的なイメージコードを示します(概念レベル)。

public sealed class AppData
{
    public string? UserName { get; set; }
    public string? Theme { get; set; }
}

サーバー側で Cookie を読み取り、AppData に詰めます。

app.MapGet("/", async context =>
{
    var cookies = context.Request.Cookies;
    var appData = new AppData
    {
        UserName = cookies["user-name"],
        Theme = cookies["theme"]
    };

    // ここで appData をプリレンダリング用のステートとして埋め込む
    // 実際には Razor コンポーネントや PersistentComponentState と組み合わせる
});

WASM 側では、JS インターフェイス経由で埋め込まれた JSON を取得し、C# の AppData にデシリアライズして利用します。 この方法のポイントは、Cookie の読み取りと HTML への埋め込みはサーバー側で完結し、WASM 側はあくまで「渡された JSON を読むだけ」に徹するところです。

代替案 2: 共通クラスライブラリでロジックだけを共有する

もうひとつの基本パターンが、共通クラスライブラリを作り、そこに共通ロジックだけを切り出しておく方法です。 Server と WASM の両方からこの共通ライブラリを参照することで、「コードは共通」「実行環境は別」という形を保ちます。

共通クラスライブラリの構成例

よくある 3 層構成のイメージです。

プロジェクト役割主な中身
MyApp.Serverサーバーサイド (ASP.NET Core / Blazor Server)Web API, DB アクセス, Cookie 読み書き, 認証など
MyApp.ClientBlazor WebAssembly クライアントUI コンポーネント, HttpClient 呼び出し, JS Interop など
MyApp.Shared共通ライブラリ (net9.0)DTO, インターフェース, バリデーションロジック, 共通ユーティリティ

重要なのは、「共有されるのは実行中のインスタンスではなく、あくまでコード(型)」という点です。 Server と Client はそれぞれが AppDataIAppDataProvider を自分のプロセス内でインスタンス化し、それぞれの環境で動作します。

たとえば、共通インターフェースを Shared に置きます。

public interface IAppDataProvider
{
    Task<AppData> GetAppDataAsync();
}

Server 側では Cookie や DB から AppData を作る実装を用意します。

public sealed class ServerAppDataProvider : IAppDataProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ServerAppDataProvider(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public Task<AppData> GetAppDataAsync()
    {
        var context = _httpContextAccessor.HttpContext!;
        var cookies = context.Request.Cookies;

        var data = new AppData
        {
            Theme = cookies["theme"],
            UserName = cookies["user-name"]
        };

        return Task.FromResult(data);
    }
}

一方、Client 側では Web API を呼ぶ実装にします。

public sealed class ClientAppDataProvider : IAppDataProvider
{
    private readonly HttpClient _http;

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

    public async Task<AppData> GetAppDataAsync()
    {
        var result = await _http.GetFromJsonAsync<AppData>("api/appdata");
        return result ?? new AppData();
    }
}

こうしておけば、WASM 側から見ても「AppData を取得する」という操作は共通インターフェースを通じて行えますし、Server 側では Cookie や DB から、Client 側では Web API からという役割分担が自然に行えます。

Cookie で取得した値を WebAssembly 側へ渡す方法

次に、「Server で読み取った Cookie の値を WASM 側へ渡す」具体的な方法について掘り下げます。

推奨: Web API(Minimal API でも可)経由で渡す

最もシンプルで安全なパターンは、Server で Cookie を読み取って Web API で JSON として返し、WASM 側が HttpClient で取得する方法です。 Cookie 本体はサーバー側で完結し、ブラウザへは「必要な情報だけ」を渡します。

Server 側のサンプル(Minimal API)

app.MapGet("/api/appdata", (HttpContext context) =>
{
    var cookies = context.Request.Cookies;

    var appData = new AppData
    {
        UserName = cookies["user-name"],
        Theme = cookies["theme"]
    };

    return Results.Json(appData);
});

ここでは HttpContext.Request.Cookies から値を読み取り、AppData に詰めて JSON として返しています。 認証が絡む場合は、このエンドポイントに [Authorize] を付ける、あるいは独自の認可ロジックを入れることで、ユーザーごとの AppData を厳密に分離できます。

Client (Blazor WASM) 側のサンプル

@inject HttpClient Http

@code {
    private AppData? appData;

    protected override async Task OnInitializedAsync()
    {
        appData = await Http.GetFromJsonAsync<AppData>("api/appdata");
    }
}

Blazor WASM では、プロジェクトテンプレートで設定される HttpClient の BaseAddress がサーバーの URL を指しているため、"api/appdata" のような相対パスで問題なく呼び出せます。 こうして取得した AppData を、アプリ全体の状態(DI でシングルトン登録したサービスなど)に保存しておけば、各コンポーネントから簡単に参照できます。

この方法のメリット

観点メリット
セキュリティCookie の生データはサーバー内で完結し、ブラウザへは必要な情報のみを JSON で返す
実装の単純さ通常の HTTP 通信として実装でき、特殊な仕組みが不要
保守性Cookie の構造を変更しても Web API の戻り値を調整するだけで済む
デバッグブラウザの開発者ツールから XHR/Fetch を確認できるので挙動がわかりやすい

別案: HTML の最初に JavaScript 変数として埋め込む

Pre-render を使わない/もしくは Server 側で HTML を生成している場合は、ページの最初の HTML に JavaScript 変数として埋め込む方法もあります。

<script>
    window.appData = {
        userName: "Taro",
        theme: "dark"
    };
</script>

WASM 側からは JS Interop で次のように読み取ります。

public class AppDataJsInterop
{
    private readonly IJSRuntime _js;

    public AppDataJsInterop(IJSRuntime js)
    {
        _js = js;
    }

    public async Task<AppData> GetAppDataAsync()
    {
        var jsObj = await _js.InvokeAsync<AppData>("eval", "window.appData");
        return jsObj ?? new AppData();
    }
}

この方法は「初回表示のタイミングで最低限の情報だけを埋め込んでおきたい」といったケースに向いています。ただし、JavaScript 変数としてブラウザ上に露出するため、認証情報や機密データなどは絶対に埋め込まないよう注意が必要です。

Cookie ではなく LocalStorage へ移行する選択肢

テーマや言語設定など、認証情報が絡まないユーザー設定値であれば、サーバー側の Cookie ではなく ブラウザの LocalStorage に保存する設計も有効です。

  1. 初回アクセス時に Web API から AppData を取得する。
  2. 取得した AppData を LocalStorage に保存する。
  3. 以降のアクセスでは LocalStorage から読み込み、必要に応じて API で同期する。

WASM 側のコード例です。

var appData = await Http.GetFromJsonAsync<AppData>("api/appdata");
await JS.InvokeVoidAsync("localStorage.setItem",
    "appData",
    JsonSerializer.Serialize(appData));

LocalStorage 方式はサーバーへのリクエストを減らせる反面、Pre-render 中には参照できない(サーバー側で実行されるためブラウザの LocalStorage にアクセスできない)という制約があります。 Pre-render を多用する設計では、初期表示時はサーバー側の AppData を使い、インタラクティブになってから LocalStorage に移すなど、ハイブリッドな設計を検討するとよいでしょう。

複数ユーザーの Cookie データをどう分離するか

「API で Cookie 情報を渡すと、ユーザー A と B のデータが混ざってしまわないか?」という不安はもっともですが、HTTP Cookie の仕様上、通常は自動的に分離されます。

HTTP Cookie の基本仕様に任せる

ブラウザは、ドメイン+パス+ブラウザプロファイル単位で Cookie を管理しています。 たとえば同じ PC でも、以下のような状況で Cookie は分離されています。

  • Chrome と Edge を使い分けている場合
  • Chrome の「ユーザープロファイル」をアカウント別に分けている場合
  • 別の OS ユーザーアカウントでログインしている場合

つまり、同じドメインにアクセスしても「誰がそのブラウザを使っているか」によって Cookie ストアは異なるので、A さんのブラウザからのリクエストに B さんの Cookie が混ざることはありません。 API の実装側では HttpContext.Request.Cookies で「そのリクエストに付いてきた Cookie だけ」を読むので、基本的にはブラウザと HTTP の仕様に従う形でユーザーが分離されます。

より強固な分離やトレーサビリティが欲しい場合

とはいえ、業務システムなどでは「仕様上分離されている」だけでは不安なケースもあります。その場合は、次のようなアプローチでより強固にします。

アプローチ概要メリット
ASP.NET Core Identity などで認証導入ログインユーザーをサーバーで確定し、そのユーザー ID 単位で Cookie / データを扱うユーザー ID を基準に一貫してデータを管理でき、権限制御も容易
サーバー発行のセッション ID 使用Guid.NewGuid() などでセッション ID を発行し、Cookie やヘッダーに載せて API と紐づけるブラウザをまたいだセッション追跡や、ログ調査がしやすくなる
Cookie のキー名にユーザー固有情報を含める例: appdata_{userId} のようにキー名にユーザー ID を含めるCookie の一覧を見たときに、どのユーザー向けかすぐ判別できる

ただし、Cookie にはサイズ制限があるため、キー名にユーザー固有情報を詰め込みすぎないよう注意してください。 基本は「認証クッキー+サーバー側の DB」でユーザーデータを持ち、Cookie にはあくまで補助的な情報だけを置くのが安全です。

Blazor から Cookie を書き込むときの注意点

Cookie を読み取るだけでなく、「Blazor アプリから Cookie を更新したい」という要件もよくあります。 ここで重要なのが、Blazor Server / WASM いずれも、アプリ起動後は自力で Set-Cookie ヘッダーを返せないという点です。

サーバー側で Cookie を書き込む場合

Cookie をサーバー側で更新するには、通常の HTTP 応答を返す必要があります。 そのため、Blazor のコンポーネントから直接 Cookie を書き換えるのではなく、次のような形になります。

  1. Blazor から Web API や MVC コントローラーに対してリクエストを送る。
  2. その Web API / コントローラーの応答の中で Set-Cookie を設定する。
  3. ブラウザは Set-Cookie を受け取って Cookie を更新する。

Minimal API での例です。

app.MapPost("/api/appdata", (HttpContext context, AppData data) =>
{
    context.Response.Cookies.Append(
        "theme",
        data.Theme ?? "light",
        new CookieOptions { Path = "/", MaxAge = TimeSpan.FromDays(30) });

    return Results.Ok();
});

Blazor コンポーネントからは、この API を呼び出すだけにしておきます。 Cookie のオプション(Secure, HttpOnly, SameSite など)はセキュリティポリシーに沿ってサーバー側で制御できるため、こちらの方が安全です。

クライアント側(JS)で Cookie を書き込む場合

もう一つの方法は、ブラウザの JavaScript から document.cookie を使って直接書き込む方法です。

document.cookie = "theme=dark; path=/; max-age=2592000";

Blazor WASM からは JS Interop を使って呼び出します。

await JS.InvokeVoidAsync("eval",
    "document.cookie = 'theme=dark; path=/; max-age=2592000'");

ただし、クライアント側から設定した Cookie には HttpOnly 属性を付けられないため、XSS 攻撃耐性を高めたいクッキー(認証クッキーなど)には向きません。 認証やセッションに関する Cookie はサーバー側でのみ発行/更新し、クライアント側から触る Cookie は UI の設定値程度に留めるのが安全な設計です。

.NET 9 / 最新 Blazor Web アプリでの設計指針まとめ

最後に、.NET 9 の Blazor Web アプリで「Server + WASM + Cookie」を扱うときの全体的な設計指針を整理しておきます。

ロジックと実行環境をきれいに分ける

  • 業務ロジックや DTO、バリデーションは共通ライブラリ(Shared)に集約する。
  • DB アクセスやファイル IO は Server 側に閉じる。
  • ブラウザ特有の処理(LocalStorage, DOM 操作, JS ライブラリ連携)は Client(WASM)に閉じる。

「共通にしたいコード」と「共通にできないコード」をしっかり切り分け、共通化するのはあくまで「フレームワークに依存しないロジック部分」に留めるのがポイントです。

Server と WASM 間の通信は標準的な HTTP に揃える

Server 側の機能を WASM から利用する際は、原則として Web API(REST 風 / Minimal API)を経由します。

  • Cookie → Server が読み取って AppData に変換 → Web API で WASM へ。
  • 設定変更 → WASM から Web API を呼び出し → Server が Cookie や DB を更新。

こうした流れを徹底することで、「ブラウザからサーバーへは HTTP のみ」「サーバーから外部リソースへは .NET ランタイムから直接」というわかりやすい責務分担になります。

プリレンダと永続ステートで初期状態を最適化する

.NET 9 の Blazor Web アプリでは、プリレンダと永続ステートを組み合わせることで、初回表示の UX を高めつつ Cookie ベースの状態を効率よく共有できます。

  • 初回リクエストで Cookie を読み、AppData を作る。
  • プリレンダリング中に AppData を HTML に埋め込む(永続ステート)。
  • WASM 起動時に JS Interop / HttpClient から AppData を取得して UI を初期化。

初回表示後の更新は Web API や LocalStorage を組み合わせ、サーバーとブラウザの役割を明確に保ったまま UX を高める設計を目指しましょう。

まとめ

  • Blazor WebAssembly から Blazor Server プロジェクトを直接参照することは、.NET 9 でもできません。実行環境が根本的に異なるためです。
  • ロジック共有は共通クラスライブラリ(Shared)で行い、「共有されるのはコードであって実行中インスタンスではない」ことを意識しましょう。
  • Server で読み取った Cookie を WASM に渡すには、Web API 経由が最もシンプルで安全です。
  • プリレンダ+永続ステートや JavaScript 変数埋め込みを組み合わせることで、初回描画時の UX を最適化できます。
  • Cookie は HTTP の仕様上ユーザーごとに分離されていますが、必要に応じて認証やセッション ID でさらに強固な分離・トレーサビリティを実現できます。
  • Cookie の書き込みは、認証などセンシティブなものは必ずサーバー側で、UI 設定などは JS 経由でといった棲み分けを行うのが安全です。

これらのポイントを押さえておけば、Blazor WebAssembly と Blazor Server を組み合わせた .NET 9 の Blazor Web アプリでも、Cookie とユーザーデータを安全かつ拡張性の高い形で扱えるようになります。 新規プロジェクトだけでなく、既存アプリのリファクタリングにもぜひ参考にしてみてください。

この記事を書いた人

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

コメント

コメントする

目次