dotnet-blazor-patterns について
Blazor のプロジェクト設定と実装パターンを扱い、.NET 開発者によるホスティングモデルの選択や、レンダリング、ルーティング、ナビゲーションの構成を支援するスキルです。ストリーミングレンダリングと WebAssembly の AOT・トリミング対応パターンも扱います。
主な機能
- 015 種類の Blazor ホスティングとレンダーモード
- 02プロジェクト設定とルーティングのパターン
- 03拡張ナビゲーションとストリーミングレンダリング
- 04AOT・トリミング対応のコーディングパターン
- 05プリレンダリングと状態の永続化
活用シーン
- 01アプリに適した Blazor ホスティングモデルの選択
- 02アプリ、ページ、コンポーネント単位でのレンダリング設定
- 03データ取得に時間がかかるページの描画
- 04WebAssembly アプリの AOT またはトリミング対応
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-blazor-patternsインストール後、Claude Code で/dotnet-blazor-patternsを入力して読み込まれたか確認します。
詳細
dotnet-blazor-patterns
Blazor のホスティングモデル、レンダーモード、プロジェクトのセットアップ、ルーティング、拡張ナビゲーション、ストリーミングレンダリング、AOT セーフなパターンを扱います。5 つすべてのホスティングモデル(InteractiveServer、InteractiveWebAssembly、InteractiveAuto、Static SSR、Hybrid)を網羅し、それぞれのトレードオフを分析します。
スコープの境界: このスキルでは、Blazor プロジェクトのセットアップ、ホスティングモデルの選択、レンダーモードの設定、ルーティング、拡張ナビゲーション、ストリーミングレンダリング、AOT セーフなパターンを扱います。コンポーネントのアーキテクチャ(ライフサイクル、状態管理、JS 相互運用、EditForm)は [skill:dotnet-blazor-components] が担当します。ホスティングモデル全体にわたる認証は [skill:dotnet-blazor-auth] が担当します。
対象外: bUnit によるコンポーネントテストについては、[skill:dotnet-blazor-testing] を参照してください。単独の SignalR パターンについては、[skill:dotnet-realtime-communication] を参照してください。ブラウザーでの E2E テストについては、[skill:dotnet-playwright] を参照してください。UI フレームワーク選択の判断ツリーについては、[skill:dotnet-ui-chooser] を参照してください。
相互参照:コンポーネントのアーキテクチャについては [skill:dotnet-blazor-components]、認証については [skill:dotnet-blazor-auth]、bUnit テストについては [skill:dotnet-blazor-testing]、単独の SignalR については [skill:dotnet-realtime-communication]、E2E テストについては [skill:dotnet-playwright]、フレームワークの選択については [skill:dotnet-ui-chooser]、アクセシビリティのパターン(ARIA、キーボードナビゲーション、スクリーンリーダー)については [skill:dotnet-accessibility] を参照してください。
ホスティングモデルとレンダーモード
Blazor Web App(.NET 8 以降)は既定のプロジェクトテンプレートであり、従来の個別の Blazor Server および Blazor WebAssembly テンプレートに代わるものです。レンダーモードはグローバル、ページ単位、またはコンポーネント単位で設定できます。
レンダーモードの概要
| レンダーモード | 属性 | インタラクティブ性 | 接続 | 最適な用途 |
|---|---|---|---|---|
| Static SSR | (none / default) | なし -- サーバーが HTML をレンダリングし、インタラクティブ性はありません | HTTP リクエストのみ | コンテンツページ、SEO、インタラクティブ性を最小限に抑えたフォーム |
| InteractiveServer | @rendermode InteractiveServer | 完全 | SignalR 回線 | 低遅延のインタラクティブ性、サーバーへの完全なアクセス、ユーザー数が少ない場合 |
| InteractiveWebAssembly | @rendermode InteractiveWebAssembly | 完全(ダウンロード後) | なし(ブラウザーで実行) | オフライン対応、ユーザー数が多い場合、サーバー負荷の軽減 |
| InteractiveAuto | @rendermode InteractiveAuto | 完全 | 最初は SignalR、その後 WASM | 両方の長所 -- すぐにインタラクティブになり、最終的にはクライアント側で実行 |
| Blazor Hybrid | MAUI/WPF/WinForms の BlazorWebView | 完全(ネイティブ) | なし(プロセス内で実行) | Web UI を備えたデスクトップ/モバイルアプリ、ネイティブ API へのアクセス |
モードごとのトレードオフ
| 考慮事項 | Static SSR | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
|---|---|---|---|---|---|
| 初回読み込み | 高速 | 高速 | 低速(WASM のダウンロード) | 高速(最初は Server) | 即時(ローカル) |
| サーバーリソース | 最小限 | ユーザーごとの回線 | ダウンロード後は不要 | 回線、その後は不要 | 不要 |
| オフライン対応 | いいえ | いいえ | はい | 部分的 | はい |
| .NET API への完全なアクセス | はい(サーバー) | はい(サーバー) | 制限あり(ブラウザーのサンドボックス) | フェーズによって異なる | はい(ネイティブ) |
| スケーラビリティ | 高い | 回線数により制限 | 高い | 高い(WASM のダウンロード後) | 該当なし(ローカル) |
| SEO | はい | プリレンダリング | プリレンダリング | プリレンダリング | 該当なし |
レンダーモードの設定
グローバル(App.razor):
<Routes @rendermode="InteractiveServer" />
ページ単位:
@page "/dashboard"
@rendermode InteractiveServer
<h1>Dashboard</h1>
コンポーネント単位:
<Counter @rendermode="InteractiveWebAssembly" />
注意点: 明示的なレンダーモード境界がない場合、子コンポーネントは親よりもインタラクティブなレンダーモードを要求できません。ただし、インタラクティブアイランドはサポートされています。Static SSR ページに埋め込まれたコンポーネントに @rendermode 属性を指定してレンダーモード境界を作成すれば、ほかの部分が静的なコンテンツの中にインタラクティブな子コンポーネントを配置できます。
プロジェクトのセットアップ
Blazor Web App(既定のテンプレート)
# Creates a Blazor Web App with InteractiveServer render mode
dotnet new blazor -n MyApp
# With specific interactivity options
dotnet new blazor -n MyApp --interactivity Auto # InteractiveAuto
dotnet new blazor -n MyApp --interactivity WebAssembly # InteractiveWebAssembly
dotnet new blazor -n MyApp --interactivity Server # InteractiveServer (default)
dotnet new blazor -n MyApp --interactivity None # Static SSR only
Blazor Web App のプロジェクト構造
MyApp/
MyApp/ # Server project
Program.cs # Host builder, services, middleware
Components/
App.razor # Root component (sets global render mode)
Routes.razor # Router component
Layout/
MainLayout.razor # Main layout
Pages/
Home.razor # Static SSR by default
Counter.razor # Can set per-page render mode
MyApp.Client/ # Client project (only if WASM or Auto)
Pages/
Counter.razor # Components that run in browser
Program.cs # WASM entry point
InteractiveAuto または InteractiveWebAssembly を使用する場合、ブラウザーで実行する必要があるコンポーネントは .Client プロジェクトに配置します。サーバープロジェクト内のコンポーネントはサーバー上でのみ実行されます。
Blazor Hybrid のセットアップ(MAUI)
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMaui>true</UseMaui>
</PropertyGroup>
</Project>
// MainPage.xaml.cs hosts BlazorWebView
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
}
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:b="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui">
<b:BlazorWebView HostPage="wwwroot/index.html">
<b:BlazorWebView.RootComponents>
<b:RootComponent Selector="#app" ComponentType="{x:Type local:Routes}" />
</b:BlazorWebView.RootComponents>
</b:BlazorWebView>
</ContentPage>
ルーティング
基本的なルーティング
@page "/products"
@page "/products/{Category}"
<h1>Products</h1>
@if (!string.IsNullOrEmpty(Category))
{
<p>Category: @Category</p>
}
@code {
[Parameter]
public string? Category { get; set; }
}
ルート制約
@page "/products/{Id:int}"
@page "/orders/{Date:datetime}"
@page "/search/{Query:minlength(3)}"
@code {
[Parameter] public int Id { get; set; }
[Parameter] public DateTime Date { get; set; }
[Parameter] public string Query { get; set; } = "";
}
クエリ文字列パラメーター
@page "/search"
@code {
[SupplyParameterFromQuery]
public string? Term { get; set; }
[SupplyParameterFromQuery(Name = "page")]
public int CurrentPage { get; set; } = 1;
}
NavigationManager
@inject NavigationManager Navigation
// Programmatic navigation
Navigation.NavigateTo("/products/electronics");
// With query string
Navigation.NavigateTo("/search?term=keyboard&page=2");
// Force full page reload (bypasses enhanced navigation)
Navigation.NavigateTo("/external-page", forceLoad: true);
拡張ナビゲーション(.NET 8+)
拡張ナビゲーションは、リンクのクリックとフォームの送信をインターセプトして、変更された DOM コンテンツのみを更新します。ページの状態を保持し、ページ全体の再読み込みを回避できます。これは Static SSR とプリレンダリングされたページに適用されます。
仕組み
- ユーザーが Blazor アプリ内のリンクをクリックします
- Blazor がナビゲーションをインターセプトします
- fetch リクエストで新しいページコンテンツを読み込みます
- Blazor が差分のみで DOM にパッチを適用します
- スクロール位置とフォーカス状態が保持されます
無効にする
<a href="/legacy-page" data-enhance-nav="false">Legacy Page</a>
<form method="post" data-enhance="false">
...
</form>
注意点: 拡張ナビゲーションは、ページ全体の読み込みを前提とするサードパーティの JavaScript ライブラリに干渉する場合があります。DOMContentLoaded で初期化される JS を含むページに移動するリンクには、data-enhance-nav="false" を使用してください。
ストリーミングレンダリング(.NET 8+)
ストリーミングレンダリングでは、初期 HTML(プレースホルダーコンテンツを含む)をすぐに送信し、その後、非同期処理の完了に合わせて更新をストリーミングします。データソースの応答が遅いページで役立ちます。
@page "/dashboard"
@attribute [StreamRendering]
<h1>Dashboard</h1>
@if (orders is null)
{
<p>Loading orders...</p>
}
else
{
<table>
@foreach (var order in orders)
{
<tr><td>@order.Id</td><td>@order.Total</td></tr>
}
</table>
}
@code {
private List<OrderDto>? orders;
protected override async Task OnInitializedAsync()
{
// Initial HTML sent immediately with "Loading orders..."
// Updated HTML streamed when this completes
orders = await OrderService.GetRecentOrdersAsync();
}
}
レンダリングモードごとの動作:
- Static SSR: ストリーミングレンダリングは初期レスポンスを送信し、その後、チャンク転送エンコーディングを介して DOM にパッチを適用します。ページはインタラクティブではありません。
- InteractiveServer/WebAssembly/Auto: 非同期処理の後にコンポーネントが自動的に再レンダリングされるため、ストリーミングレンダリングの効果は限定的です。
[StreamRendering]属性は主にプリレンダリングフェーズで効果を発揮します。
AOT セーフなパターン
Native AOT(事前コンパイル)または IL トリミングを使用して Blazor WebAssembly をターゲットにする場合は、実行時のリフレクションに依存するパターンを避けてください。
ソースジェネレーターを優先したシリアル化
// CORRECT: Source-generated JSON serialization (AOT-compatible)
[JsonSerializable(typeof(ProductDto))]
[JsonSerializable(typeof(List<ProductDto>))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
public partial class AppJsonContext : JsonSerializerContext { }
// Register in Program.cs
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
// Usage in HttpClient calls
var products = await Http.GetFromJsonAsync<List<ProductDto>>(
"/api/products",
AppJsonContext.Default.ListProductDto);
// WRONG: Reflection-based serialization (fails under AOT/trimming)
var products = await Http.GetFromJsonAsync<List<ProductDto>>("/api/products");
トリミングに安全な JS 相互運用
// CORRECT: Use IJSRuntime with explicit method names (no dynamic dispatch)
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "key", "value");
var value = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "key");
// CORRECT: Use IJSObjectReference for module imports (.NET 8+)
var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./js/chart.js");
await module.InvokeVoidAsync("initChart", elementRef, data);
await module.DisposeAsync();
// WRONG: Dynamic dispatch via reflection (trimmed away)
// var method = typeof(JSRuntime).GetMethod("InvokeAsync");
// method.MakeGenericMethod(returnType).Invoke(...)
リンカーの構成
<ItemGroup>
<TrimmerRootAssembly Include="MyApp.Client" />
</ItemGroup>
トリミングによる削除を防ぐ必要がある型の場合:
// Mark types that are accessed via reflection
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public class DynamicFormModel
{
// Properties discovered at runtime for form generation
public string Name { get; set; } = "";
public int Age { get; set; }
}
避けるべきアンチパターン
- リフレクションベースの DI -- サービスの解決に
Activator.CreateInstanceやType.GetTypeを使用しないでください。明示的な登録を行い、組み込みの DI コンテナーを使用してください。 - 動的な型読み込み -- 実行時に
Assembly.LoadやAssembly.GetTypes()を使用しないでください。すべての型を起動時に登録してください。 - 実行時のコード生成 --
System.Reflection.EmitやSystem.Linq.Expressions.Expression.Compile()を使用しないでください。代わりにソースジェネレーターを使用してください。 - 型指定のない JSON デシリアル化 --
JsonSerializerContextを指定せずにJsonSerializer.Deserialize<T>(json)を使用しないでください。必ずソース生成されたコンテキストを指定してください。
プリレンダリング
プリレンダリングでは、インタラクティブなランタイムが読み込まれる前にサーバー上で HTML を生成します。これにより、体感パフォーマンスと SEO が向上します。
インタラクティブモードでのプリレンダリング
<Counter @rendermode="InteractiveServer" />
デフォルトでは、インタラクティブコンポーネントはプリレンダリングされます。無効にするには:
@rendermode @(new InteractiveServerRenderMode(prerender: false))
プリレンダリングをまたいだ状態の保持
プリレンダリング中に計算された状態は、コンポーネントがインタラクティブに再初期化されると失われます。状態を保持するには、PersistentComponentState を使用してください:
@inject PersistentComponentState ApplicationState
@implements IDisposable
@code {
private List<ProductDto>? products;
private PersistingComponentStateSubscription _subscription;
protected override async Task OnInitializedAsync()
{
_subscription = ApplicationState.RegisterOnPersisting(PersistState);
if (!ApplicationState.TryTakeFromJson<List<ProductDto>>(
"products", out var restored))
{
products = await ProductService.GetProductsAsync();
}
else
{
products = restored;
}
}
private Task PersistState()
{
ApplicationState.PersistAsJson("products", products);
return Task.CompletedTask;
}
public void Dispose() => _subscription.Dispose();
}
.NET 10 の安定機能
これらの機能は、net10.0 TFM が検出された場合に利用できます。安定しており、プレビュー機能のオプトインは不要です。
WebAssembly のプリロード
.NET 10 では、InteractiveAuto の Server フェーズ中に WebAssembly アセンブリを blazor.web.js でプリロードできるようになりました。ユーザーが最初にページを読み込むと、Server 回線がインタラクティブ機能を処理する間に、WASM ランタイムとアプリのアセンブリがバックグラウンドでダウンロードされます。アセンブリはすでにキャッシュされているため、以降のナビゲーションではより速く WASM に切り替わります。
拡張フォーム検証
.NET 10 では EditForm の検証が拡張され、エラーメッセージの書式設定が改善されるとともに、Static SSR フォームで IValidatableObject がサポートされます。拡張フォーム処理(Enhance 属性)を使用した場合も、ページ全体を再読み込みすることなく、検証メッセージが正しく表示されます。
// IValidatableObject works in Static SSR enhanced forms in .NET 10
public sealed class OrderModel : IValidatableObject
{
[Required]
public string ProductId { get; set; } = "";
[Range(1, 100)]
public int Quantity { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
if (ProductId == "DISCONTINUED" && Quantity > 0)
{
yield return new ValidationResult(
"Cannot order discontinued products",
[nameof(ProductId), nameof(Quantity)]);
}
}
}
Blazor 診断ミドルウェア
.NET 10 では、開発中に Blazor の回線とコンポーネントの状態を調査するための MapBlazorDiagnostics ミドルウェアが追加されています。
// Program.cs -- available in .NET 10
if (app.Environment.IsDevelopment())
{
app.MapBlazorDiagnostics(); // Exposes /_blazor/diagnostics endpoint
}
診断エンドポイントでは、アクティブな回線、コンポーネントツリー、レンダーモードの割り当て、タイミングデータを確認できます。開発中にレンダーモードの境界やコンポーネントのライフサイクルに関する問題をデバッグするために使用します。
エージェントが注意すべき点
- すべてのページで InteractiveServer を既定にしないでください。 静的 SSR が既定かつ最も効率的なレンダーモードです。ユーザーの操作が必要な場合にのみ、インタラクティブ性を追加してください。
- WASM を対象とするコンポーネントをサーバープロジェクトに配置しないでください。 ブラウザーで実行する必要があるコンポーネント(InteractiveWebAssembly または InteractiveAuto)は、
.Clientプロジェクトに配置します。 - プリレンダリング時に
PersistentComponentStateを忘れないでください。 これがないと、プリレンダリング中に取得したデータが破棄され、コンポーネントがインタラクティブになったときに再取得されるため、目に見えるちらつきが発生します。 - WASM でリフレクションベースのシリアル化を使用しないでください。 AOT 互換性とトリミングの安全性を確保するため、常に
JsonSerializerContextとソース生成シリアライザーを使用してください。 - Blazor アプリから離れる場合を除き、ナビゲーションを強制読み込みしないでください。
NavigateTo("/page", forceLoad: true)は拡張ナビゲーションを回避し、ページ全体を再読み込みします。 - インタラクティブレンダーモードを不適切にネストしないでください。 子コンポーネントは、親よりもインタラクティブ性の高いモードを要求できません。レイアウトから順に、レンダーモードの境界を計画してください。
前提条件
- .NET 8.0 以降(Blazor Web App テンプレート、レンダーモード、拡張ナビゲーション、ストリーミングレンダリング)
- 安定版の拡張機能を使用する場合は .NET 10.0(WebAssembly の事前読み込み、拡張フォーム検証、診断ミドルウェア)
- Blazor Hybrid 用の MAUI ワークロード(
dotnet workload install maui)