mcpField
F
fetch-and-send-data

fetch-and-send-data

github.com/dotnet開発5.8k3.2k 回インストール1 日前に更新
リポジトリ

fetch-and-send-data について

Blazor 開発者向けに、データ取得・送信やコンポーネントの読み込み処理をまとめたスキルガイドです。レンダリングモード別のデータアクセスに加え、キャンセル、エラー、パラメーター変更時の処理方法を扱います。

主な機能

  1. 01レンダリングモード別のデータアクセス選択
  2. 02名前付き・型付き HttpClient の設定
  3. 03静的 SSR とプリレンダリングへの対応
  4. 04ErrorBoundary によるエラー管理
  5. 05パラメーター変更時のキャンセルと再読み込み
  6. 06HTTP によるデータ送信

活用シーン

  1. 01Blazor ページで外部 API のデータを読み込む
  2. 02静的 SSR ページにストリーミング読み込みを設定する
  3. 03ルート変更後の重複リクエストに対処する
  4. 04フォーム送信時の二重操作を防ぐ

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dotnet/skills --skill fetch-and-send-data

インストール後、Claude Code で/fetch-and-send-dataを入力して読み込まれたか確認します。

詳細

データの取得と送信

ステップ 1 — AGENTS.md を読む

インタラクティビティモードとスコープを確認します。

モードデータアクセス
なし(静的 SSR)サーバー側:サービス/DbContext を注入します。読み込み中の UX には [StreamRendering] を使用します。
Serverサーバー側:サービス/DbContext を注入します。??= と [PersistentState] を使ってプリレンダリングに対応します。
WebAssemblyブラウザー側:HttpClient のみ使用します。サーバーへの直接アクセスはできません。
Autoサーバーとブラウザーの両方。必ず API を経由します。

ステップ 2 — HttpClient を登録する

Server から外部 API を呼び出す場合、または WebAssembly/Auto を使用する場合に必要です。自分のデータベースにアクセスするサーバーコンポーネントでは、DbContext またはサービスを直接注入してください。

// Named client — requires Microsoft.Extensions.Http NuGet
builder.Services.AddHttpClient("CatalogAPI", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
});

// Typed client
builder.Services.AddHttpClient<CatalogClient>(client =>
    client.BaseAddress = new Uri("https://api.example.com/"));

プリレンダリングを行う WebAssembly/Auto では、サーバーと .Client の Program.cs の両方に登録してください。

ステップ 3 — データを取得する

シンプルな読み込み
@page "/products"
@inject CatalogClient Catalog

@if (products is null)
{
    <p>Loading…</p>
}
else
{
    @foreach (var p in products)
    {
        <p>@p.Name — @p.Price.ToString("C")</p>
    }
}

@code {
    private Product[]? products;

    protected override async Task OnInitializedAsync()
    {
        products = await Catalog.GetProductsAsync();
    }
}

最もシンプルなケースでは、エラー処理は必要ありません。親またはレイアウトレベルでコンポーネントの使用箇所を <ErrorBoundary> で囲み、未処理の例外を捕捉してください。

静的 SSR — StreamRendering

[StreamRendering] がない場合、OnInitializedAsync が完了するまでユーザーには何も表示されません。

@attribute [StreamRendering]

これは静的 SSR にのみ適用されます。インタラクティブコンポーネントには影響しません。

プリレンダリングへの対応

プリレンダリングでは OnInitializedAsync が 2 回呼び出されます。重複を避けるには、次のようにします。

[PersistentState] private Product[]? products;

protected override async Task OnInitializedAsync()
{
    products ??= await Catalog.GetProductsAsync();
}

詳細は support-prerendering スキルを参照してください。

ステップ 4 — エラーを処理する

デフォルトのエラー戦略として <ErrorBoundary> を使用します。コンポーネントごとに catch 処理を追加しなくても、すべてのコンポーネントで一貫したエラー体験を実現できます。レイアウトまたは親レベルでコンポーネントの使用箇所を囲んでください。

<ErrorBoundary>
    <ChildContent>
        <ProductList />
    </ChildContent>
    <ErrorContent>
        <div class="alert alert-danger">Something went wrong. Please refresh.</div>
    </ErrorContent>
</ErrorBoundary>

HttpRequestException などのキャンセル以外の例外は、自動的に ErrorBoundary に伝播するため、コンポーネントに catch ブロックを追加する必要はありません。

キャンセルは例外です

ComponentBase は、自身によるキャンセル(破棄、パラメーターの変更)と外部によるキャンセル(HttpClient のタイムアウト)の両方について、すべての OperationCanceledException を暗黙的に無視します。そのため、ErrorBoundary には通知されません。つまり、次のようになります。

  • 自身によるキャンセル → 暗黙的に無視されます。これは正しい動作なので、対応は不要です。
  • 外部によるキャンセル(タイムアウト) → こちらも暗黙的に無視されます。コンポーネントは読み込み中の状態で止まりますが、タイムアウトはまれなため、通常は問題ありません。
コンポーネント内にエラー処理を追加する場合

コンポーネントで ErrorBoundary では対応できない動作が必要な場合にのみ、catch ブロックを追加します。通常は再試行やタイムアウト専用メッセージが該当します。その場合も、必要なものだけを捕捉してください。

// Catch only external cancellation (timeouts) — everything else flows to ErrorBoundary
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
    Logger.LogWarning(ex, "Request timed out for category {CategoryId}", CategoryId);
    error = "The request timed out. Please try again.";
}

ErrorBoundary に処理を任せるのではなく、コンポーネント内で一般的なエラーを処理し、再試行ボタンを表示する必要がある場合は、次のようにします。

catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
    Logger.LogWarning(ex, "Request timed out for category {CategoryId}", CategoryId);
    error = "The request timed out. Please try again.";
}
catch (Exception ex)
{
    Logger.LogError(ex, "Failed to load products for category {CategoryId}", CategoryId);
    error = "Unable to load products. Please try again.";
}
ルール
  • exception.Message を表示しないでください — 個人情報、接続文字列、または内部情報が含まれる可能性があります。ユーザーにわかりやすいメッセージを固定で使用してください。
  • 必ず ILogger を通じてログを記録してください — 実際の例外はログのパイプラインに送られます。
  • サービスでは CancellationToken を受け取るようにしてください — すべての非同期呼び出しに渡し、コンポーネントのキャンセル時に処理を停止できるようにします。

ステップ 5 — パラメーターに応じて再読み込みする

データが、変化するルートパラメーターまたはクエリパラメーター(例:/products/1 と /products/2 の間の移動)に依存する場合は、OnParametersSetAsync を使用します。データに影響しないパラメーターでは再読み込みをスキップするため、ガードを追加してください。

パターン:キャンセルして再読み込みし、古いデータを重ねて表示する
@page "/products/{CategoryId:int}"
@implements IAsyncDisposable
@inject ProductService ProductService
@inject ILogger<Products> Logger

@if (error is not null)
{
    <div class="alert alert-danger">
        <p>@error</p>
        <button @onclick="LoadAsync">Retry</button>
    </div>
}
else if (products is null)
{
    <p>Loading…</p>
}
else
{
    @if (isLoading)
    {
        <p><em>Refreshing…</em></p>
    }
    @foreach (var p in products)
    {
        <p>@p.Name — @p.Price.ToString("C")</p>
    }
}

@code {
    [Parameter] public int CategoryId { get; set; }
    [SupplyParameterFromQuery] public string? ViewMode { get; set; } // UI-only

    private CancellationTokenSource? cts;
    private int? loadedCategoryId;
    private List<Product>? products;
    private bool isLoading;
    private string? error;

    protected override async Task OnParametersSetAsync()
    {
        if (CategoryId == loadedCategoryId)
        {
            return; // Only ViewMode changed — no reload
        }

        loadedCategoryId = CategoryId;
        await LoadAsync();
    }

    private async Task LoadAsync()
    {
        if (cts is not null)
        {
            await cts.CancelAsync();
            cts.Dispose();
        }

        cts = new CancellationTokenSource();
        var cancellationToken = cts.Token; // Capture locally before await

        error = null;
        isLoading = true;

        try
        {
            var result = await ProductService.GetByCategoryAsync(CategoryId, cancellationToken);
            products = result;
        }
        catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
        {
            Logger.LogWarning(ex, "Timed out loading category {CategoryId}", CategoryId);
            error = "The request timed out. Please try again.";
        }
        finally
        {
            isLoading = false;
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (cts is not null)
        {
            await cts.CancelAsync();
            cts.Dispose();
        }
    }
}

主なポイント:

  • 追跡値でガードする:loadedCategoryId により、UI パラメーターのみが変更された場合の再読み込みをスキップします。
  • await の前にトークンをローカル変数に保存する — 並行するパラメーター変更によって CTS フィールドが置き換わる可能性があります。
  • 後続の読み込みでは products を null にしない — isLoading オーバーレイを表示し、既存データを表示したままにします。
  • IAsyncDisposable により、ユーザーが画面を離れるときに保留中の処理をキャンセルします。

ステップ 6 — データを送信する

var response = await http.PostAsJsonAsync("products", newProduct);
response.EnsureSuccessStatusCode();

var response = await http.PutAsJsonAsync($"products/{id}", updated);
response.EnsureSuccessStatusCode();

var response = await http.DeleteAsync($"products/{id}");
response.EnsureSuccessStatusCode();

重複リクエストを防ぐため、保存中は送信ボタンを無効にします。保存中のインジケーターを表示してください。

ステップ 7 — プリレンダリングを使用する Auto モードまたは WebAssembly 向けのサービス抽象化

コンポーネントがサーバーとブラウザーの両方で実行される場合(Auto モード、またはプリレンダリングを使用する WebAssembly)は、抽象基底クラスを通じてデータアクセスを抽象化します。

public abstract class ProductServiceBase
{
    public abstract Task<Product[]> GetAllAsync(CancellationToken ct = default);
}

// Server — direct database access
public class ServerProductService(AppDbContext db) : ProductServiceBase
{
    public override async Task<Product[]> GetAllAsync(CancellationToken ct = default) =>
        await db.Products.ToArrayAsync(ct);
}

// Client — calls API
public class ClientProductService(HttpClient http) : ProductServiceBase
{
    public override async Task<Product[]> GetAllAsync(CancellationToken ct = default) =>
        await http.GetFromJsonAsync<Product[]>("api/products", ct) ?? [];
}

各プロジェクトの Program.cs で、適切な実装を登録します。コンポーネントでは抽象基底クラスをインジェクションします。

やってはいけないこと

  • コンストラクターで API を呼び出さないでください — OnInitializedAsync を使用します。
  • データが変更されるパラメーターに依存する場合を除き、OnParametersSetAsync を使用しないでください。 初回の読み込みには OnInitializedAsync を使用します。
  • WebAssembly/Auto モードのコンポーネントに DbContext をインジェクションしないでください — ブラウザーにデータベースはありません。
  • HttpClient を使って自分のサーバーを呼び出さないでください — サービスを直接インジェクションします。
  • ユーザーに exception.Message を表示しないでください — 個人情報漏えいのリスクがあります。ログに記録し、一般的なメッセージを表示します。
  • 自分でキャンセルする場合に OperationCanceledException をキャッチしないでください — ComponentBase が処理します。

コメント