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 Server | Blazor 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 を読み取り、永続ステートとして埋め込む
大まかな流れは次のようになります。
- ブラウザからの初回リクエストに Cookie が付与されてサーバーに到達する。
- サーバー側(Blazor Server 部分)が Cookie を読み取り、
AppDataなどのモデルにまとめる。 - プリレンダリング中にその
AppDataを永続ステート(例: Persistent Component State)として HTML 内に埋め込む。 - 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.Client | Blazor WebAssembly クライアント | UI コンポーネント, HttpClient 呼び出し, JS Interop など |
| MyApp.Shared | 共通ライブラリ (net9.0) | DTO, インターフェース, バリデーションロジック, 共通ユーティリティ |
重要なのは、「共有されるのは実行中のインスタンスではなく、あくまでコード(型)」という点です。 Server と Client はそれぞれが AppData や IAppDataProvider を自分のプロセス内でインスタンス化し、それぞれの環境で動作します。
たとえば、共通インターフェースを 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 に保存する設計も有効です。
- 初回アクセス時に Web API から AppData を取得する。
- 取得した AppData を LocalStorage に保存する。
- 以降のアクセスでは 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 を書き換えるのではなく、次のような形になります。
- Blazor から Web API や MVC コントローラーに対してリクエストを送る。
- その Web API / コントローラーの応答の中で
Set-Cookieを設定する。 - ブラウザは
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 とユーザーデータを安全かつ拡張性の高い形で扱えるようになります。 新規プロジェクトだけでなく、既存アプリのリファクタリングにもぜひ参考にしてみてください。

コメント