fetch-and-send-data

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Fetch and Send Data

获取与发送数据

Step 1 — Read AGENTS.md

步骤1 — 阅读AGENTS.md

Check Interactivity Mode and Scope:
ModeData access
None (Static SSR)Server-side: inject services/
DbContext
. Use
[StreamRendering]
for loading UX.
ServerServer-side: inject services/
DbContext
. Guard prerender with
??=
+
[PersistentState]
.
WebAssemblyBrowser-side:
HttpClient
only. No direct server access.
AutoBoth server and browser. Always go through an API.
查看交互模式适用范围
模式数据访问方式
None (静态SSR)服务端:注入服务/
DbContext
。使用
[StreamRendering]
实现加载交互。
Server服务端:注入服务/
DbContext
。使用
??=
+
[PersistentState]
避免预渲染重复操作。
WebAssembly浏览器端:仅使用
HttpClient
。无法直接访问服务端。
Auto同时支持服务端和浏览器端。需始终通过API访问数据。

Step 2 — Register HttpClient

步骤2 — 注册HttpClient

Only needed when calling external APIs from Server, or always for WebAssembly/Auto. Server components accessing their own database should inject
DbContext
or a service directly.
csharp
// 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/"));
For WebAssembly/Auto with prerendering, register in both server and
.Client
Program.cs
.
仅在从Server调用外部API时需要,或者在WebAssembly/Auto模式下始终需要。访问自身数据库的Server组件应直接注入
DbContext
或服务。
csharp
// 命名客户端 — 需要Microsoft.Extensions.Http NuGet包
builder.Services.AddHttpClient("CatalogAPI", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
});

// 类型化客户端
builder.Services.AddHttpClient<CatalogClient>(client =>
    client.BaseAddress = new Uri("https://api.example.com/"));
对于启用预渲染的WebAssembly/Auto模式,需在服务端和
.Client
项目的
Program.cs
中都进行注册。

Step 3 — Fetch Data

步骤3 — 获取数据

Simple load

简单加载

razor
@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();
    }
}
No error handling needed in the simplest case — wrap the component usage in
<ErrorBoundary>
at the parent/layout level to catch unhandled exceptions.
razor
@page "/products"
@inject CatalogClient Catalog

@if (products is null)
{
    <p>加载中…</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>
包裹组件即可捕获未处理异常。

Static SSR — StreamRendering

静态SSR — StreamRendering

Without
[StreamRendering]
, the user sees nothing until
OnInitializedAsync
completes:
razor
@attribute [StreamRendering]
Only affects Static SSR. No effect on interactive components.
如果不使用
[StreamRendering]
,用户需等待
OnInitializedAsync
执行完成才能看到内容:
razor
@attribute [StreamRendering]
仅对静态SSR生效,对交互式组件无影响。

Prerendering guard

预渲染防护

Prerendering calls
OnInitializedAsync
twice. Skip the duplicate:
csharp
[PersistentState] private Product[]? products;

protected override async Task OnInitializedAsync()
{
    products ??= await Catalog.GetProductsAsync();
}
See the
support-prerendering
skill for details.
预渲染会调用两次
OnInitializedAsync
。可通过以下方式避免重复执行:
csharp
[PersistentState] private Product[]? products;

protected override async Task OnInitializedAsync()
{
    products ??= await Catalog.GetProductsAsync();
}
详情请参考
support-prerendering
技能文档。

Step 4 — Handle Errors

步骤4 — 错误处理

Use
<ErrorBoundary>
as the default error strategy. It provides a consistent error experience across all components without any per-component catch logic. Wrap component usage at the layout or parent level:
razor
<ErrorBoundary>
    <ChildContent>
        <ProductList />
    </ChildContent>
    <ErrorContent>
        <div class="alert alert-danger">Something went wrong. Please refresh.</div>
    </ErrorContent>
</ErrorBoundary>
Non-cancellation exceptions (
HttpRequestException
, etc.) propagate to
ErrorBoundary
automatically — no catch blocks needed in the component.
默认策略是使用
<ErrorBoundary>
。它无需为每个组件编写捕获逻辑,即可在所有组件中提供一致的错误体验。在布局或父组件层包裹目标组件:
razor
<ErrorBoundary>
    <ChildContent>
        <ProductList />
    </ChildContent>
    <ErrorContent>
        <div class="alert alert-danger">出现错误,请刷新页面重试。</div>
    </ErrorContent>
</ErrorBoundary>
非取消类异常(如
HttpRequestException
等)会自动传递给
ErrorBoundary
——组件中无需编写catch块。

Cancellation is special

取消操作的特殊性

ComponentBase
silently swallows all
OperationCanceledException
— both self-initiated (disposal, parameter change) and external (HttpClient timeout).
ErrorBoundary
never sees them. This means:
  • Self-cancellation → silently ignored. Correct behavior, no action needed.
  • External cancellation (timeout) → also silently swallowed. Component gets stuck in loading state. Usually acceptable — timeouts are rare.
ComponentBase
会自动忽略所有
OperationCanceledException
——包括主动触发的(组件销毁、参数变更)和外部触发的(HttpClient超时)。
ErrorBoundary
永远不会捕获这类异常。这意味着:
  • 主动取消→自动忽略,属于正确行为,无需处理。
  • 外部取消(超时)→同样被自动忽略,组件会停留在加载状态。通常可接受——超时情况较为罕见。

When to add in-component error handling

何时添加组件内错误处理

Only add catch blocks when the component needs behavior
ErrorBoundary
can't provide — typically retries or timeout-specific messages. Even then, only catch what you need:
csharp
// 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.";
}
If the component also needs to handle general errors with a retry button instead of letting
ErrorBoundary
take over:
csharp
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.";
}
仅当组件需要
ErrorBoundary
无法提供的行为时才添加catch块——通常是重试超时专属提示。即便如此,也仅捕获需要处理的异常:
csharp
// 仅捕获外部取消(超时)——其他异常全部传递给ErrorBoundary
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
    Logger.LogWarning(ex, "分类{CategoryId}请求超时", CategoryId);
    error = "请求超时,请重试。";
}
如果组件还需要通过重试按钮处理通用错误,而非交由
ErrorBoundary
接管:
csharp
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
    Logger.LogWarning(ex, "分类{CategoryId}请求超时", CategoryId);
    error = "请求超时,请重试。";
}
catch (Exception ex)
{
    Logger.LogError(ex, "加载分类{CategoryId}产品失败", CategoryId);
    error = "无法加载产品,请重试。";
}

Rules

规则

  • Never display
    exception.Message
    — it may contain PII, connection strings, or internal details. Use hardcoded user-friendly messages.
  • Always log through
    ILogger
    — the real exception goes to the logging pipeline.
  • Services must accept
    CancellationToken
    — pass it to every async call so work stops when the component cancels.
  • 绝不要显示
    exception.Message
    ——它可能包含个人身份信息(PII)、连接字符串或内部细节。使用硬编码的友好提示信息。
  • 始终通过
    ILogger
    记录日志
    ——真实异常需传入日志管道。
  • 服务必须接受
    CancellationToken
    ——将其传递给所有异步调用,以便组件取消时停止工作。

Step 5 — Parameter-Driven Reloading

步骤5 — 参数驱动的重新加载

When data depends on a route or query parameter that changes (e.g., navigating between
/products/1
and
/products/2
), use
OnParametersSetAsync
with a guard to skip reloads for parameters that don't affect data.
当数据依赖于路由或查询参数(例如在
/products/1
/products/2
之间导航)时,使用
OnParametersSetAsync
并添加防护逻辑,避免因不影响数据的参数变更而触发重新加载。

Pattern: cancel-and-reload with stale data overlay

模式:取消并重新加载,同时保留旧数据显示

razor
@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();
        }
    }
}
Key details:
  • Guard with tracked value:
    loadedCategoryId
    skips reloads when only UI parameters change.
  • Capture the token locally before the await — the CTS field may be replaced by a concurrent parameter change.
  • Don't null out
    products
    on subsequent loads — keep existing data visible with an
    isLoading
    overlay.
  • IAsyncDisposable
    cancels pending work when the user navigates away.
razor
@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">重试</button>
    </div>
}
else if (products is null)
{
    <p>加载中…</p>
}
else
{
    @if (isLoading)
    {
        <p><em>刷新中…</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

    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; // 仅ViewMode变更——无需重新加载
        }

        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; // 在await前本地捕获令牌

        error = null;
        isLoading = true;

        try
        {
            var result = await ProductService.GetByCategoryAsync(CategoryId, cancellationToken);
            products = result;
        }
        catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
        {
            Logger.LogWarning(ex, "加载分类{CategoryId}超时", CategoryId);
            error = "请求超时,请重试。";
        }
        finally
        {
            isLoading = false;
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (cts is not null)
        {
            await cts.CancelAsync();
            cts.Dispose();
        }
    }
}
关键细节:
  • 跟踪值防护
    loadedCategoryId
    可避免仅UI参数变更时触发重新加载。
  • 本地捕获令牌:在await前捕获令牌——并发参数变更可能会替换CTS字段。
  • 重新加载时不清空
    products
    :保留现有数据并显示
    isLoading
    覆盖层。
  • IAsyncDisposable
    :用户导航离开时取消待处理任务。

Step 6 — Send Data

步骤6 — 发送数据

csharp
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();
Disable the submit button while saving to prevent duplicate requests. Show a saving indicator.
csharp
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();
保存期间禁用提交按钮以防止重复请求,并显示保存状态指示器。

Step 7 — Service Abstraction for Auto or WebAssembly with Prerendering

步骤7 — 为Auto或带预渲染的WebAssembly构建服务抽象

When components run in both server and browser (Auto mode, or WebAssembly with prerendering), abstract data access behind an abstract base class:
csharp
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) ?? [];
}
Register the appropriate implementation in each project's
Program.cs
. Components inject the abstract base class.
当组件同时在服务端和浏览器端运行时(Auto模式,或带预渲染的WebAssembly模式),需将数据访问抽象为抽象基类:
csharp
public abstract class ProductServiceBase
{
    public abstract Task<Product[]> GetAllAsync(CancellationToken ct = default);
}

// 服务端 — 直接访问数据库
public class ServerProductService(AppDbContext db) : ProductServiceBase
{
    public override async Task<Product[]> GetAllAsync(CancellationToken ct = default) =>
        await db.Products.ToArrayAsync(ct);
}

// 客户端 — 调用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
中注册对应的实现。组件注入抽象基类即可。

Don'ts

禁忌

  • Don't call APIs in constructors — use
    OnInitializedAsync
    .
  • Don't use
    OnParametersSetAsync
    unless data depends on a changing parameter.
    Use
    OnInitializedAsync
    for initial loads.
  • Don't inject
    DbContext
    in WebAssembly/Auto components
    — no database in the browser.
  • Don't call your own server via
    HttpClient
    — inject the service directly.
  • Don't display
    exception.Message
    to users
    — PII risk. Log it, show a generic message.
  • Don't catch
    OperationCanceledException
    for self-cancellation
    ComponentBase
    handles it.
  • 不要在构造函数中调用API——请使用
    OnInitializedAsync
  • 不要使用
    OnParametersSetAsync
    ,除非数据依赖于可变参数
    。初始加载请使用
    OnInitializedAsync
  • 不要在WebAssembly/Auto组件中注入
    DbContext
    ——浏览器中无数据库。
  • 不要通过
    HttpClient
    调用自身服务端
    ——请直接注入服务。
  • 不要向用户显示
    exception.Message
    ——存在PII泄露风险。记录日志并显示通用提示。
  • 不要捕获主动取消的
    OperationCanceledException
    ——
    ComponentBase
    会自动处理。