fetch-and-send-data
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFetch and Send Data
获取与发送数据
Step 1 — Read AGENTS.md
步骤1 — 阅读AGENTS.md
Check Interactivity Mode and Scope:
| Mode | Data access |
|---|---|
| None (Static SSR) | Server-side: inject services/ |
| Server | Server-side: inject services/ |
| WebAssembly | Browser-side: |
| Auto | Both server and browser. Always go through an API. |
查看交互模式和适用范围:
| 模式 | 数据访问方式 |
|---|---|
| None (静态SSR) | 服务端:注入服务/ |
| Server | 服务端:注入服务/ |
| WebAssembly | 浏览器端:仅使用 |
| 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 or a service directly.
DbContextcsharp
// 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 .
.ClientProgram.cs仅在从Server调用外部API时需要,或者在WebAssembly/Auto模式下始终需要。访问自身数据库的Server组件应直接注入或服务。
DbContextcsharp
// 命名客户端 — 需要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模式,需在服务端和项目的中都进行注册。
.ClientProgram.csStep 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 at the parent/layout level to catch unhandled exceptions.
<ErrorBoundary>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 , the user sees nothing until completes:
[StreamRendering]OnInitializedAsyncrazor
@attribute [StreamRendering]Only affects Static SSR. No effect on interactive components.
如果不使用,用户需等待执行完成才能看到内容:
[StreamRendering]OnInitializedAsyncrazor
@attribute [StreamRendering]仅对静态SSR生效,对交互式组件无影响。
Prerendering guard
预渲染防护
Prerendering calls twice. Skip the duplicate:
OnInitializedAsynccsharp
[PersistentState] private Product[]? products;
protected override async Task OnInitializedAsync()
{
products ??= await Catalog.GetProductsAsync();
}See the skill for details.
support-prerendering预渲染会调用两次。可通过以下方式避免重复执行:
OnInitializedAsynccsharp
[PersistentState] private Product[]? products;
protected override async Task OnInitializedAsync()
{
products ??= await Catalog.GetProductsAsync();
}详情请参考技能文档。
support-prerenderingStep 4 — Handle Errors
步骤4 — 错误处理
Use 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:
<ErrorBoundary>razor
<ErrorBoundary>
<ChildContent>
<ProductList />
</ChildContent>
<ErrorContent>
<div class="alert alert-danger">Something went wrong. Please refresh.</div>
</ErrorContent>
</ErrorBoundary>Non-cancellation exceptions (, etc.) propagate to automatically — no catch blocks needed in the component.
HttpRequestExceptionErrorBoundary默认策略是使用。它无需为每个组件编写捕获逻辑,即可在所有组件中提供一致的错误体验。在布局或父组件层包裹目标组件:
<ErrorBoundary>razor
<ErrorBoundary>
<ChildContent>
<ProductList />
</ChildContent>
<ErrorContent>
<div class="alert alert-danger">出现错误,请刷新页面重试。</div>
</ErrorContent>
</ErrorBoundary>非取消类异常(如等)会自动传递给——组件中无需编写catch块。
HttpRequestExceptionErrorBoundaryCancellation is special
取消操作的特殊性
ComponentBaseOperationCanceledExceptionErrorBoundary- 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.
ComponentBaseOperationCanceledExceptionErrorBoundary- 主动取消→自动忽略,属于正确行为,无需处理。
- 外部取消(超时)→同样被自动忽略,组件会停留在加载状态。通常可接受——超时情况较为罕见。
When to add in-component error handling
何时添加组件内错误处理
Only add catch blocks when the component needs behavior can't provide — typically retries or timeout-specific messages. Even then, only catch what you need:
ErrorBoundarycsharp
// 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 take over:
ErrorBoundarycsharp
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.";
}仅当组件需要无法提供的行为时才添加catch块——通常是重试或超时专属提示。即便如此,也仅捕获需要处理的异常:
ErrorBoundarycsharp
// 仅捕获外部取消(超时)——其他异常全部传递给ErrorBoundary
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
Logger.LogWarning(ex, "分类{CategoryId}请求超时", CategoryId);
error = "请求超时,请重试。";
}如果组件还需要通过重试按钮处理通用错误,而非交由接管:
ErrorBoundarycsharp
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
Logger.LogWarning(ex, "分类{CategoryId}请求超时", CategoryId);
error = "请求超时,请重试。";
}
catch (Exception ex)
{
Logger.LogError(ex, "加载分类{CategoryId}产品失败", CategoryId);
error = "无法加载产品,请重试。";
}Rules
规则
- Never display — it may contain PII, connection strings, or internal details. Use hardcoded user-friendly messages.
exception.Message - Always log through — the real exception goes to the logging pipeline.
ILogger - Services must accept — pass it to every async call so work stops when the component cancels.
CancellationToken
- 绝不要显示——它可能包含个人身份信息(PII)、连接字符串或内部细节。使用硬编码的友好提示信息。
exception.Message - 始终通过记录日志——真实异常需传入日志管道。
ILogger - 服务必须接受——将其传递给所有异步调用,以便组件取消时停止工作。
CancellationToken
Step 5 — Parameter-Driven Reloading
步骤5 — 参数驱动的重新加载
When data depends on a route or query parameter that changes (e.g., navigating between and ), use with a guard to skip reloads for parameters that don't affect data.
/products/1/products/2OnParametersSetAsync当数据依赖于路由或查询参数(例如在和之间导航)时,使用并添加防护逻辑,避免因不影响数据的参数变更而触发重新加载。
/products/1/products/2OnParametersSetAsyncPattern: 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: skips reloads when only UI parameters change.
loadedCategoryId - Capture the token locally before the await — the CTS field may be replaced by a concurrent parameter change.
- Don't null out on subsequent loads — keep existing data visible with an
productsoverlay.isLoading - cancels pending work when the user navigates away.
IAsyncDisposable
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();
}
}
}关键细节:
- 跟踪值防护:可避免仅UI参数变更时触发重新加载。
loadedCategoryId - 本地捕获令牌:在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 . Components inject the abstract base class.
Program.cs当组件同时在服务端和浏览器端运行时(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.csDon'ts
禁忌
- Don't call APIs in constructors — use .
OnInitializedAsync - Don't use unless data depends on a changing parameter. Use
OnParametersSetAsyncfor initial loads.OnInitializedAsync - Don't inject in WebAssembly/Auto components — no database in the browser.
DbContext - Don't call your own server via — inject the service directly.
HttpClient - Don't display to users — PII risk. Log it, show a generic message.
exception.Message - Don't catch for self-cancellation —
OperationCanceledExceptionhandles it.ComponentBase
- 不要在构造函数中调用API——请使用。
OnInitializedAsync - 不要使用,除非数据依赖于可变参数。初始加载请使用
OnParametersSetAsync。OnInitializedAsync - 不要在WebAssembly/Auto组件中注入——浏览器中无数据库。
DbContext - 不要通过调用自身服务端——请直接注入服务。
HttpClient - 不要向用户显示——存在PII泄露风险。记录日志并显示通用提示。
exception.Message - 不要捕获主动取消的——
OperationCanceledException会自动处理。ComponentBase