mirror of
https://github.com/dotnet/skills.git
synced 2026-09-20 09:49:54 +08:00
Add blazor skills to dotnet-blazor plugin (#357)
* Add dotnet-blazor plugin and convert-blazor-server-to-webapp skill Add the dotnet-blazor plugin with 9 skills covering Blazor Web App development: - plan-ui-change: Plan and scaffold UI features in Blazor Web Apps - create-blazor-project: Create new Blazor projects with proper render mode setup - author-component: Author Razor components with parameters, events, lifecycle - coordinate-components: Share state across components using CascadingValueSource/scoped services - use-js-interop: Call JavaScript from Blazor and vice versa - fetch-and-send-data: HTTP data access with proper service patterns - configure-auth: Set up ASP.NET Core Identity and authorization in Blazor - collect-user-input: Build forms with EditForm, validation, and file uploads - support-prerendering: Handle prerendering lifecycle and state persistence Add convert-blazor-server-to-webapp skill to the dotnet-aspnet plugin for migrating .NET 7 Blazor Server apps to .NET 8+ Blazor Web App architecture. All 10 skills pass evaluation with quality improvements ranging from 12-50% and overfitting scores within acceptable thresholds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update eng/allowed-external-deps.txt * Update .gitignore --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
da5e63e878
commit
198e58c983
@@ -59,6 +59,11 @@
|
||||
"source": "./plugins/dotnet-aspnet",
|
||||
"description": "ASP.NET Core web development skills including middleware, endpoints, real-time communication, and API patterns."
|
||||
},
|
||||
{
|
||||
"name": "dotnet-blazor",
|
||||
"source": "./plugins/dotnet-blazor",
|
||||
"description": "Skills for Blazor development: component authoring, interactivity, and web application patterns."
|
||||
},
|
||||
{
|
||||
"name": "dotnet11",
|
||||
"source": "./plugins/dotnet11",
|
||||
|
||||
@@ -62,6 +62,11 @@
|
||||
"source": "./plugins/dotnet-aspnet",
|
||||
"description": "ASP.NET Core web development skills including middleware, endpoints, real-time communication, and API patterns."
|
||||
},
|
||||
{
|
||||
"name": "dotnet-blazor",
|
||||
"source": "./plugins/dotnet-blazor",
|
||||
"description": "Skills for Blazor development: component authoring, interactivity, and web application patterns."
|
||||
},
|
||||
{
|
||||
"name": "dotnet11",
|
||||
"source": "./plugins/dotnet11",
|
||||
|
||||
@@ -85,6 +85,10 @@
|
||||
/plugins/dotnet-aspnet/ @dotnet/aspnet
|
||||
/tests/dotnet-aspnet/ @dotnet/aspnet
|
||||
|
||||
# dotnet-blazor (Blazor component and app development)
|
||||
/plugins/dotnet-blazor/ @dotnet/aspnet
|
||||
/tests/dotnet-blazor/ @dotnet/aspnet
|
||||
|
||||
# dotnet-data (data access, Entity Framework)
|
||||
/plugins/dotnet-data/skills/optimizing-ef-core-queries/ @dotnet/efteam @dotnet/skills-data-reviewers
|
||||
/tests/dotnet-data/optimizing-ef-core-queries/ @dotnet/efteam @dotnet/skills-data-reviewers
|
||||
|
||||
@@ -59,6 +59,11 @@
|
||||
"source": "./plugins/dotnet-aspnet",
|
||||
"description": "ASP.NET Core web development skills including middleware, endpoints, real-time communication, and API patterns."
|
||||
},
|
||||
{
|
||||
"name": "dotnet-blazor",
|
||||
"source": "./plugins/dotnet-blazor",
|
||||
"description": "Skills for Blazor development: component authoring, interactivity, and web application patterns."
|
||||
},
|
||||
{
|
||||
"name": "dotnet11",
|
||||
"source": "./plugins/dotnet11",
|
||||
|
||||
@@ -430,3 +430,4 @@ vally-results/
|
||||
|
||||
# Roslyn / C# language server cache files
|
||||
*.lscache
|
||||
.dotnet/
|
||||
|
||||
@@ -21,6 +21,7 @@ This repository contains the .NET team's curated set of core skills and custom a
|
||||
| [dotnet-template-engine](plugins/dotnet-template-engine/) | .NET Template Engine skills: template discovery, project scaffolding, and template authoring. |
|
||||
| [dotnet-test](plugins/dotnet-test/) | Skills for running, diagnosing, and migrating .NET tests: test execution, filtering, platform detection, and MSTest workflows. |
|
||||
| [dotnet-aspnet](plugins/dotnet-aspnet/) | ASP.NET Core web development skills including middleware, endpoints, real-time communication, and API patterns. |
|
||||
| [dotnet-blazor](plugins/dotnet-blazor/) | Skills for Blazor development: component authoring, interactivity, and web application patterns. |
|
||||
| [dotnet11](plugins/dotnet11/) | Skills for new .NET 11 APIs and language features. |
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
---
|
||||
name: convert-blazor-server-to-webapp
|
||||
license: MIT
|
||||
description: >
|
||||
Guides conversion of a pre-.NET 8 Blazor Server app into a .NET 8+ Blazor Web App.
|
||||
USE FOR: migrating apps that use AddServerSideBlazor and MapBlazorHub to the
|
||||
AddRazorComponents/MapRazorComponents model, converting _Host.cshtml to an App.razor
|
||||
root component, replacing blazor.server.js with blazor.web.js, migrating
|
||||
CascadingAuthenticationState to a service, adopting new Blazor Web App features
|
||||
like enhanced navigation and streaming rendering.
|
||||
DO NOT USE FOR: apps that are already Blazor Web Apps (already use AddRazorComponents
|
||||
and MapRazorComponents), Blazor WebAssembly or hosted Blazor WebAssembly apps
|
||||
(different migration path), apps that should stay on the Blazor Server hosting
|
||||
model without converting, or apps still targeting .NET Framework.
|
||||
---
|
||||
|
||||
# Convert Blazor Server App to Blazor Web App
|
||||
|
||||
This skill helps an agent convert a pre-.NET 8 Blazor Server app into a .NET 8+ Blazor Web App. The old hosting model uses `AddServerSideBlazor`/`MapBlazorHub` with a `_Host.cshtml` Razor Page as the entry point. The new Blazor Web App model uses `AddRazorComponents`/`MapRazorComponents` with an `App.razor` root component, enabling per-component render modes, enhanced navigation, streaming rendering, and other .NET 8+ features. The converted app uses `InteractiveServer` render mode to preserve existing interactive behavior.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Migrating a Blazor Server app from .NET 6 or .NET 7 to .NET 8+
|
||||
- App currently uses `AddServerSideBlazor()` and `MapBlazorHub()` in `Program.cs` (or `Startup.cs`)
|
||||
- App uses `Pages/_Host.cshtml` (or `_Host.razor`) as the host page with Component Tag Helpers
|
||||
- Want to adopt new Blazor Web App features while keeping interactive server rendering
|
||||
|
||||
## When Not to Use
|
||||
|
||||
- **The app already uses `AddRazorComponents` and `MapRazorComponents`.** It is already a Blazor Web App — no conversion is needed. Stop here and tell the user the app is already using the Blazor Web App model.
|
||||
- Blazor WebAssembly or hosted Blazor WebAssembly app — these have a different migration path
|
||||
- The app should stay on the legacy Blazor Server hosting model (just update TFM and packages)
|
||||
- The app targets .NET Framework — it must be migrated to .NET first
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| Blazor Server project | Yes | The `.csproj` and source files of the Blazor Server app |
|
||||
| Target framework | Yes | .NET 8 or later (e.g., `net8.0`, `net9.0`, `net10.0`) |
|
||||
| `Program.cs` or `Startup.cs` | Yes | The app's service and middleware configuration |
|
||||
| `_Host.cshtml` location | Recommended | Usually `Pages/_Host.cshtml`; may be `_Host.razor` in some projects |
|
||||
|
||||
## Workflow
|
||||
|
||||
> **Commit strategy:** Commit after each logical step so the migration is reviewable and bisectable.
|
||||
|
||||
### Step 1: Update the project file
|
||||
|
||||
Update the `.csproj` file:
|
||||
|
||||
1. Change the Target Framework Moniker (TFM) to the target version:
|
||||
```xml
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
```
|
||||
2. Update all `Microsoft.AspNetCore.*`, `Microsoft.EntityFrameworkCore.*`, `Microsoft.Extensions.*`, and `System.Net.Http.Json` package references to the matching version.
|
||||
|
||||
For non-Blazor project file changes (nullable reference types, implicit usings, HTTP/3 support, etc.), see the [general ASP.NET Core migration guide](https://learn.microsoft.com/aspnet/core/migration/70-to-80).
|
||||
|
||||
### Step 2: Create `Routes.razor` from `App.razor`
|
||||
|
||||
The old `App.razor` contains the `<Router>` component. This content moves to a new `Routes.razor` file so that `App.razor` can become the root HTML document component.
|
||||
|
||||
1. Create a new file `Routes.razor` in the project root.
|
||||
2. Move the entire content of `App.razor` into `Routes.razor`.
|
||||
3. If the content is wrapped in `<CascadingAuthenticationState>`, remove that wrapper (it will be replaced by a service in Step 5).
|
||||
4. Leave `App.razor` empty for the next step.
|
||||
|
||||
The resulting `Routes.razor` should look similar to:
|
||||
|
||||
```razor
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
```
|
||||
|
||||
If the app uses `<AuthorizeRouteView>` instead of `<RouteView>`, keep it — it works the same way in Blazor Web Apps.
|
||||
|
||||
### Step 3: Convert `_Host.cshtml` to `App.razor`
|
||||
|
||||
Move the HTML shell from `Pages/_Host.cshtml` into the now-empty `App.razor` and transform it from a Razor Page into a Razor component:
|
||||
|
||||
1. **Remove Razor Page directives** — delete `@page "/"`, `@using Microsoft.AspNetCore.Components.Web`, `@namespace`, and `@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers`.
|
||||
|
||||
2. **Add component injection** — if using environment-conditional error UI, add:
|
||||
```razor
|
||||
@inject IHostEnvironment Env
|
||||
```
|
||||
|
||||
3. **Fix the base tag** — replace `<base href="~/" />` with `<base href="/" />`.
|
||||
|
||||
4. **Replace HeadOutlet Component Tag Helper** — replace:
|
||||
```html
|
||||
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
|
||||
```
|
||||
with:
|
||||
```razor
|
||||
<HeadOutlet @rendermode="InteractiveServer" />
|
||||
```
|
||||
|
||||
5. **Replace App Component Tag Helper with Routes** — replace:
|
||||
```html
|
||||
<component type="typeof(App)" render-mode="ServerPrerendered" />
|
||||
```
|
||||
with:
|
||||
```razor
|
||||
<Routes @rendermode="InteractiveServer" />
|
||||
```
|
||||
|
||||
6. **Replace Environment Tag Helpers** — replace:
|
||||
```html
|
||||
<environment include="Staging,Production">
|
||||
An error has occurred. This application may no longer respond until reloaded.
|
||||
</environment>
|
||||
<environment include="Development">
|
||||
An unhandled exception has occurred. See browser dev tools for details.
|
||||
</environment>
|
||||
```
|
||||
with:
|
||||
```razor
|
||||
@if (Env.IsDevelopment())
|
||||
{
|
||||
<text>
|
||||
An unhandled exception has occurred. See browser dev tools for details.
|
||||
</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>
|
||||
An error has occurred. This app may no longer respond until reloaded.
|
||||
</text>
|
||||
}
|
||||
```
|
||||
|
||||
7. **Update the Blazor script** — replace:
|
||||
```html
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
```
|
||||
with:
|
||||
```html
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
```
|
||||
|
||||
8. **Add render mode import** — add to `_Imports.razor`:
|
||||
```razor
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
```
|
||||
|
||||
9. **Delete `Pages/_Host.cshtml`** (and `Pages/_Host.cshtml.cs` if it exists).
|
||||
|
||||
**Prerendering note:** If the original app used `render-mode="Server"` (not `"ServerPrerendered"`), prerendering was disabled. Preserve this by using `new InteractiveServerRenderMode(prerender: false)` instead of `InteractiveServer` for both `HeadOutlet` and `Routes`.
|
||||
|
||||
### Step 4: Update `Program.cs`
|
||||
|
||||
Make the following changes to `Program.cs` (or `Startup.cs` if the app uses the older hosting pattern):
|
||||
|
||||
1. **Replace Blazor Server services** — replace:
|
||||
```csharp
|
||||
builder.Services.AddServerSideBlazor();
|
||||
```
|
||||
with:
|
||||
```csharp
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
```
|
||||
|
||||
If `AddServerSideBlazor` had options configured (e.g., circuit options, hub options, detailed errors), migrate them to `AddInteractiveServerComponents`:
|
||||
```csharp
|
||||
// Old:
|
||||
builder.Services.AddServerSideBlazor(options =>
|
||||
{
|
||||
options.DetailedErrors = true;
|
||||
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(10);
|
||||
});
|
||||
|
||||
// New:
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents(options =>
|
||||
{
|
||||
options.DetailedErrors = true;
|
||||
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(10);
|
||||
});
|
||||
```
|
||||
|
||||
2. **Replace Blazor endpoint mapping** — replace:
|
||||
```csharp
|
||||
app.MapBlazorHub();
|
||||
```
|
||||
with:
|
||||
```csharp
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
```
|
||||
|
||||
Ensure there is a `using` statement for the project's root namespace so that `App` resolves to the `App.razor` component.
|
||||
|
||||
3. **Remove the fallback route** — delete:
|
||||
```csharp
|
||||
app.MapFallbackToPage("/_Host");
|
||||
```
|
||||
|
||||
4. **Remove explicit routing middleware** — delete if present:
|
||||
```csharp
|
||||
app.UseRouting();
|
||||
```
|
||||
Endpoint routing is the default and explicit `UseRouting()` is no longer needed.
|
||||
|
||||
5. **Add antiforgery middleware** — add after `UseAuthentication`/`UseAuthorization` if present:
|
||||
```csharp
|
||||
app.UseAntiforgery();
|
||||
```
|
||||
`AddRazorComponents` registers antiforgery services automatically, but the middleware must be explicitly added to the pipeline. Without it, form POST requests fail with 400 errors.
|
||||
|
||||
### Step 5: Migrate `CascadingAuthenticationState` (if present)
|
||||
|
||||
If the app used `<CascadingAuthenticationState>` to wrap the router:
|
||||
|
||||
1. Remove the `<CascadingAuthenticationState>` component wrapper (already done in Step 2 if following this workflow).
|
||||
2. Add the cascading authentication state service in `Program.cs`:
|
||||
```csharp
|
||||
builder.Services.AddCascadingAuthenticationState();
|
||||
```
|
||||
|
||||
The component wrapper approach does not work across render mode boundaries in Blazor Web Apps. The service-based approach provides `Task<AuthenticationState>` as a cascading value to all components regardless of render mode.
|
||||
|
||||
### Step 6: Recommended improvements (optional)
|
||||
|
||||
These are optional modernization improvements — not required for the conversion to work. If you suggest any of these, state explicitly that they are optional.
|
||||
|
||||
- **Replace `UseStaticFiles` with `MapStaticAssets`** (.NET 9+): `app.MapStaticAssets()` provides optimized static file serving with fingerprinting, pre-compression, and content-based ETags. See [MapStaticAssets documentation](https://learn.microsoft.com/aspnet/core/fundamentals/static-files#mapstaticassets).
|
||||
- **Add `@attribute [StreamRendering]`** to pages with async data loading (`OnInitializedAsync`) for improved perceived performance. The page renders its initial synchronous content immediately and re-renders when async data arrives.
|
||||
- **Update CSS isolation bundle reference** if the `<link>` tag referenced a `_Host` assembly name; ensure it matches the project's actual assembly name: `<link href="{AssemblyName}.styles.css" rel="stylesheet" />`.
|
||||
- For other non-Blazor improvements (minimal hosting, HTTP/3, output caching, etc.), see the [general ASP.NET Core migration guide](https://learn.microsoft.com/aspnet/core/migration/70-to-80).
|
||||
|
||||
### Step 7: Verify the migration
|
||||
|
||||
1. Build the project targeting the new framework. Confirm no compile errors.
|
||||
2. Search for remaining references to removed APIs:
|
||||
- `AddServerSideBlazor`
|
||||
- `MapBlazorHub`
|
||||
- `MapFallbackToPage`
|
||||
- `blazor.server.js`
|
||||
- `_Host.cshtml`
|
||||
3. Run the app and verify:
|
||||
- Pages load and render correctly
|
||||
- Interactive features work (forms, event handlers, SignalR circuits)
|
||||
- Navigation between pages works
|
||||
- Authentication and authorization flows work if present
|
||||
4. Run existing tests.
|
||||
|
||||
## Validation
|
||||
|
||||
- [ ] No references to `AddServerSideBlazor` remain
|
||||
- [ ] No references to `MapBlazorHub` remain
|
||||
- [ ] No references to `MapFallbackToPage("/_Host")` remain
|
||||
- [ ] No references to `blazor.server.js` remain
|
||||
- [ ] `Pages/_Host.cshtml` has been deleted
|
||||
- [ ] `App.razor` serves as the root component with a full HTML document structure
|
||||
- [ ] `Routes.razor` contains the `<Router>` configuration
|
||||
- [ ] `Program.cs` uses `AddRazorComponents().AddInteractiveServerComponents()`
|
||||
- [ ] `Program.cs` uses `MapRazorComponents<App>().AddInteractiveServerRenderMode()`
|
||||
- [ ] `app.UseAntiforgery()` is present in the middleware pipeline
|
||||
- [ ] If the app used `<CascadingAuthenticationState>`, it has been replaced with `AddCascadingAuthenticationState()` service registration
|
||||
- [ ] App builds and runs successfully on the target framework
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
| Pitfall | Solution |
|
||||
|---------|----------|
|
||||
| Missing `UseAntiforgery()` middleware | `AddRazorComponents` registers antiforgery services, but the middleware must be explicitly added. Place `app.UseAntiforgery()` after `UseAuthentication`/`UseAuthorization`. Without it, form POST requests fail with 400 errors. |
|
||||
| Forgetting to replace `blazor.server.js` with `blazor.web.js` | The old script does not work with the Blazor Web App model. Replace all references to `_framework/blazor.server.js` with `_framework/blazor.web.js`. |
|
||||
| Not removing `<CascadingAuthenticationState>` wrapper | The component wrapper does not work across render mode boundaries in Blazor Web Apps. Use `builder.Services.AddCascadingAuthenticationState()` instead. |
|
||||
| Leaving `app.UseRouting()` in the pipeline | Explicit `UseRouting()` is no longer needed and can interfere with endpoint routing. Remove it unless other middleware specifically requires it. |
|
||||
| Using `InteractiveServer` when prerendering was disabled | If the original app used `render-mode="Server"` (not `"ServerPrerendered"`), use `new InteractiveServerRenderMode(prerender: false)` to preserve the same behavior. Using `InteractiveServer` enables prerendering which can cause unexpected issues with components that depend on JS interop during initialization. |
|
||||
| Not migrating `AddServerSideBlazor` circuit options | If circuit options, hub options, or detailed error settings were configured, migrate them to `AddInteractiveServerComponents(options => { ... })`. Otherwise those settings are silently lost. |
|
||||
| `UseAntiforgery()` placed before authentication middleware | The antiforgery middleware must be placed after `UseAuthentication` and `UseAuthorization`. Placing it before causes antiforgery validation to run before the user identity is established. |
|
||||
| CSS isolation bundle link has wrong assembly name | If the `<link href="{Name}.styles.css">` tag referenced the old project name, update it to match the current assembly name. |
|
||||
|
||||
## More Info
|
||||
|
||||
- [Convert a Blazor Server app into a Blazor Web App](https://learn.microsoft.com/aspnet/core/migration/70-to-80#convert-a-blazor-server-app-into-a-blazor-web-app) — the official step-by-step migration guide
|
||||
- [ASP.NET Core Blazor render modes](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) — understanding InteractiveServer, InteractiveWebAssembly, and InteractiveAuto
|
||||
- [Migrate CascadingAuthenticationState to services](https://learn.microsoft.com/aspnet/core/migration/70-to-80#migrate-the-cascadingauthenticationstate-component-to-cascading-authentication-state-services) — replacing the component wrapper with a service
|
||||
- [MapStaticAssets](https://learn.microsoft.com/aspnet/core/fundamentals/static-files#mapstaticassets) — optimized static file serving in .NET 9+
|
||||
- [Migrate from ASP.NET Core 7.0 to 8.0](https://learn.microsoft.com/aspnet/core/migration/70-to-80) — general migration guide for all ASP.NET Core changes
|
||||
- [Stream rendering with Blazor](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes#streaming-rendering) — `@attribute [StreamRendering]` for async data loading
|
||||
- [Cascading values and render mode boundaries](https://learn.microsoft.com/aspnet/core/blazor/components/cascading-values-and-parameters#cascading-valuesparameters-and-render-mode-boundaries) — why cascading parameters do not cross render mode boundaries
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "dotnet-blazor",
|
||||
"version": "0.1.0",
|
||||
"description": "Skills for Blazor development: component authoring, interactivity, and web application patterns.",
|
||||
"skills": ["./skills/"]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
license: MIT
|
||||
name: author-component
|
||||
description: >
|
||||
Create or review Blazor components (.razor files) with correct architecture.
|
||||
USE FOR: writing new Blazor components that do NOT involve JavaScript interop,
|
||||
implementing parameters and EventCallback, RenderFragment slots, component
|
||||
lifecycle (OnInitializedAsync, OnParametersSet), async patterns, IAsyncDisposable,
|
||||
CancellationToken, CSS isolation, code-behind.
|
||||
DO NOT USE FOR: creating new projects (use create-blazor-project), JavaScript
|
||||
interop or calling browser APIs from Blazor (use use-js-interop), forms and
|
||||
validation (use collect-user-input), prerendering issues (use support-prerendering),
|
||||
HTTP data fetching patterns (use fetch-and-send-data), coordinating state between
|
||||
unrelated components (use coordinate-components).
|
||||
---
|
||||
|
||||
# Author Blazor Component
|
||||
|
||||
## Core Rules
|
||||
|
||||
- Data flows **down** via `[Parameter]`. Events flow **up** via `EventCallback<T>` (never `Action`/`Func`).
|
||||
- Never mutate `[Parameter]` properties. Copy to a private field in `OnParametersSet`.
|
||||
- Use `[Parameter] public T Prop { get; set; }` — never `required` or `init` (causes BL0007).
|
||||
- Use `[EditorRequired]` for required parameters.
|
||||
- Handle all states: loading, empty, loaded, error — each with `@if`/`@else`.
|
||||
- Use `@key` on repeated elements in loops for efficient diffing.
|
||||
- Use `IReadOnlyList<T>` (not `IEnumerable<T>`) for collection parameters.
|
||||
|
||||
## RenderFragment & Generics
|
||||
|
||||
```csharp
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
[Parameter] public RenderFragment<TItem>? RowTemplate { get; set; } // generic template
|
||||
```
|
||||
|
||||
Use `@typeparam TItem` for generic components.
|
||||
|
||||
## File Patterns
|
||||
|
||||
- **Single-file:** `.razor` with `@code` block when logic < ~50 lines.
|
||||
- **Code-behind:** `.razor` + `.razor.cs` with `partial class` when logic > ~50 lines.
|
||||
|
||||
## Disposal
|
||||
|
||||
Implement `IAsyncDisposable` (not `IDisposable`) when the component owns subscriptions, timers, or CTS.
|
||||
In `DisposeAsync`: unsubscribe (`-=`), cancel CTS, dispose resources. Never call `StateHasChanged`.
|
||||
|
||||
## Async Patterns
|
||||
|
||||
- `await` every async operation. Never use `.Result`, `.Wait()`, `Task.Run`, `ContinueWith`, `Thread.Start`.
|
||||
- **Debounce:** `Task.Delay` + `CancellationTokenSource`. Cancel old CTS, create new, await delay, do work. Never use `System.Threading.Timer` or `System.Timers.Timer`.
|
||||
- **Polling:** Loop in `OnInitializedAsync` with `await Task.Delay(interval, token)` — stays on sync context.
|
||||
- **External events** (`Action<T>`): Use `async void` handler + `await InvokeAsync(() => { state++; StateHasChanged(); })` + `catch` → `DispatchExceptionAsync`. Never `_ = InvokeAsync(...)`.
|
||||
- Cancel CTS in `DisposeAsync`. Don't catch `ObjectDisposedException` — use CTS cancellation.
|
||||
|
||||
## Don'ts
|
||||
|
||||
- `required`/`init` on `[Parameter]` — runtime failure
|
||||
- Mutate `[Parameter]` — copy to private field in `OnParametersSet`
|
||||
- `Action`/`Func` for events — use `EventCallback<T>`
|
||||
- `Task.Run`/`.Result`/`.Wait()`/Timer for debounce — deadlock or thread-pool escape
|
||||
- Inline `style` attributes — use CSS classes or `data-*` attributes
|
||||
- `catch { throw; }` — use `when` guard or let exceptions propagate
|
||||
- Gold-plating: ARIA, wrapper divs, accessibility features not requested
|
||||
- `_ = InvokeAsync(...)` — swallows exceptions; use `async void` + `DispatchExceptionAsync`
|
||||
@@ -0,0 +1,256 @@
|
||||
# Async Programming Rules
|
||||
|
||||
Blazor's sync context guarantees single-threaded component execution. All rules below follow from this.
|
||||
|
||||
## Await every Task
|
||||
|
||||
`await` every `Task` by default — discarded tasks silently lose exceptions. The only exception: fire-and-forget where the called method wraps its body in `try/catch` and routes errors via `DispatchExceptionAsync` (see Fire-and-Forget section below).
|
||||
|
||||
```csharp
|
||||
// DO
|
||||
private async Task LoadData()
|
||||
{
|
||||
items = await Http.GetFromJsonAsync<List<Item>>("api/items");
|
||||
}
|
||||
|
||||
// DON'T — fire-and-forget hides exceptions
|
||||
private void LoadData()
|
||||
{
|
||||
_ = Http.GetFromJsonAsync<List<Item>>("api/items");
|
||||
}
|
||||
```
|
||||
|
||||
## Forbidden Primitives
|
||||
|
||||
These deadlock or escape the sync context. Never use in components:
|
||||
|
||||
| Forbidden | Why |
|
||||
|-----------|-----|
|
||||
| `Thread.Start` / `new Thread` | Escapes sync context |
|
||||
| `Task.Run` | Offloads to thread-pool; `StateHasChanged` throws |
|
||||
| `.Result` / `.Wait()` | Deadlocks sync context |
|
||||
| `Task.ContinueWith` | Continuation runs outside sync context |
|
||||
| `Channel<T>`, `BlockingCollection<T>`, concurrent collections | Unnecessary — single-threaded access guaranteed |
|
||||
|
||||
```csharp
|
||||
// DON'T — Task.Run escapes sync context
|
||||
_ = Task.Run(async () => {
|
||||
var result = await OrderService.SubmitAsync(order);
|
||||
StateHasChanged(); // InvalidOperationException!
|
||||
});
|
||||
|
||||
// DO — stay on sync context
|
||||
private async Task ProcessOrder()
|
||||
{
|
||||
var result = await OrderService.SubmitAsync(order);
|
||||
message = result.Message;
|
||||
}
|
||||
```
|
||||
|
||||
## StateHasChanged
|
||||
|
||||
Framework auto-renders after lifecycle methods and event handlers complete. Don't call `StateHasChanged` routinely.
|
||||
|
||||
**Call only for:**
|
||||
|
||||
1. **Intermediate updates** between multiple awaits:
|
||||
```csharp
|
||||
private async Task ProcessSteps()
|
||||
{
|
||||
status = "Step 1...";
|
||||
await Step1Async();
|
||||
status = "Step 2...";
|
||||
StateHasChanged(); // intermediate update
|
||||
await Step2Async();
|
||||
}
|
||||
```
|
||||
|
||||
2. **External events** (timer, C# event, WebSocket) via `InvokeAsync`:
|
||||
```csharp
|
||||
private async void OnExternalEvent(object? sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await InvokeAsync(() => { count++; StateHasChanged(); });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DispatchExceptionAsync(ex);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`InvokeAsync` marshals onto the sync context. `StateHasChanged` from a raw thread throws `InvalidOperationException`. Use `async void` for external event handlers — it's the only place `async void` is appropriate in Blazor. Always `await InvokeAsync` and route errors via `DispatchExceptionAsync`.
|
||||
|
||||
## Fire-and-Forget
|
||||
|
||||
Route errors via `DispatchExceptionAsync` (activates error boundaries, logs like lifecycle exceptions):
|
||||
|
||||
```csharp
|
||||
private void SendReport() => _ = SendReportCore();
|
||||
|
||||
private async Task SendReportCore()
|
||||
{
|
||||
try { await ReportSender.SendAsync(); }
|
||||
catch (Exception ex) { await DispatchExceptionAsync(ex); }
|
||||
}
|
||||
```
|
||||
|
||||
## Alternatives to Forbidden Primitives
|
||||
|
||||
**Instead of `Task.Run`** — use `await` directly or `Task.Yield`:
|
||||
|
||||
```csharp
|
||||
// Yield to let renderer paint, then continue on sync context
|
||||
private async Task StartLongOperation()
|
||||
{
|
||||
status = "Starting...";
|
||||
await Task.Yield();
|
||||
await LongOperationService.RunAsync();
|
||||
status = "Done!";
|
||||
}
|
||||
```
|
||||
|
||||
**Chunked CPU work** — break with `Task.Yield` so UI stays responsive:
|
||||
|
||||
```csharp
|
||||
private async Task ProcessLargeList()
|
||||
{
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
ProcessItem(items[i]);
|
||||
if (i % 100 == 0)
|
||||
{
|
||||
StateHasChanged();
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Indivisible long ops** — `Task.WhenAny` + `Task.Delay` for progress:
|
||||
|
||||
```csharp
|
||||
private async Task RunLongQuery()
|
||||
{
|
||||
var queryTask = DatabaseService.RunExpensiveQueryAsync();
|
||||
while (queryTask != await Task.WhenAny(queryTask, Task.Delay(1000)))
|
||||
{
|
||||
status = "Still working...";
|
||||
StateHasChanged();
|
||||
}
|
||||
result = await queryTask;
|
||||
}
|
||||
```
|
||||
|
||||
### Instead of `.Result` / `.Wait()` — use `await`
|
||||
|
||||
```csharp
|
||||
// Wrong — blocks the sync context, deadlocks the circuit
|
||||
private void Load()
|
||||
{
|
||||
var data = Http.GetFromJsonAsync<List<Item>>("api/items").Result;
|
||||
}
|
||||
|
||||
// Correct — use async all the way through
|
||||
private async Task Load()
|
||||
{
|
||||
var data = await Http.GetFromJsonAsync<List<Item>>("api/items");
|
||||
}
|
||||
```
|
||||
|
||||
When the calling context is synchronous and cannot be changed to `async` (e.g., an interface method that returns `void`), use fire-and-forget with error handling:
|
||||
|
||||
```csharp
|
||||
private void Load()
|
||||
{
|
||||
_ = LoadAsync();
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
data = await Http.GetFromJsonAsync<List<Item>>("api/items");
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DispatchExceptionAsync(ex);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`StateHasChanged` is required here because the framework does not know about the fire-and-forget task, so it will not trigger a re-render when it completes.
|
||||
|
||||
### Instead of `ConcurrentDictionary` / `Channel<T>` — use plain collections
|
||||
|
||||
Because the synchronization context guarantees single-threaded access within a circuit, regular `Dictionary<K,V>`, `List<T>`, and `Queue<T>` are safe. Concurrent collections add overhead with no benefit:
|
||||
|
||||
```csharp
|
||||
// Wrong — unnecessary overhead, hides the threading model
|
||||
private readonly ConcurrentDictionary<string, int> cache = new();
|
||||
|
||||
// Correct — the sync context already prevents concurrent access
|
||||
private readonly Dictionary<string, int> cache = [];
|
||||
```
|
||||
|
||||
### Instead of `Task.ContinueWith` — use `await` with code after it
|
||||
|
||||
```csharp
|
||||
// Wrong — continuation may run on a thread-pool thread
|
||||
private void Start()
|
||||
{
|
||||
_ = Http.GetFromJsonAsync<List<Item>>("api/items")
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
items = t.Result;
|
||||
StateHasChanged(); // InvalidOperationException!
|
||||
});
|
||||
}
|
||||
|
||||
// Correct — straightforward async/await
|
||||
private async Task Start()
|
||||
{
|
||||
items = await Http.GetFromJsonAsync<List<Item>>("api/items");
|
||||
}
|
||||
```
|
||||
|
||||
## Cancelling async work with CancellationToken
|
||||
|
||||
Components that start long-running async operations (HTTP calls, database queries, streaming) should cancel that work when the component is disposed — typically when the user navigates away.
|
||||
|
||||
Use a `CancellationTokenSource` that is cancelled in `DisposeAsync`:
|
||||
|
||||
```razor
|
||||
@implements IAsyncDisposable
|
||||
@inject HttpClient Http
|
||||
|
||||
<p>@status</p>
|
||||
|
||||
@code {
|
||||
private string status = "Loading...";
|
||||
private CancellationTokenSource cts = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await Http.GetFromJsonAsync<List<Item>>(
|
||||
"api/items", cts.Token);
|
||||
status = $"Loaded {data?.Count} items.";
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Component was disposed while loading — expected, nothing to do.
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
cts.Cancel();
|
||||
cts.Dispose();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
# Breaking Down Components
|
||||
|
||||
## Sibling Decomposition
|
||||
|
||||
When a component has two independent blocks (no shared state/handlers), extract each as a sibling.
|
||||
|
||||
```razor
|
||||
<!-- CardTitle.razor -->
|
||||
<div class="card-header">
|
||||
<h3>@Title</h3>
|
||||
<button @onclick="OnPin">Pin</button>
|
||||
</div>
|
||||
@code {
|
||||
[Parameter, EditorRequired] public string Title { get; set; } = "";
|
||||
[Parameter] public EventCallback OnPin { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
```razor
|
||||
<!-- CardBody.razor -->
|
||||
<div class="card-body">
|
||||
<p>@Description</p>
|
||||
<button @onclick="OnExpand">Read more</button>
|
||||
</div>
|
||||
@code {
|
||||
[Parameter, EditorRequired] public string Description { get; set; } = "";
|
||||
[Parameter] public EventCallback OnExpand { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
```razor
|
||||
<!-- Card.razor — composes siblings -->
|
||||
<div class="card">
|
||||
<CardTitle Title="@Title" OnPin="OnPin" />
|
||||
<CardBody Description="@Description" OnExpand="OnExpand" />
|
||||
</div>
|
||||
```
|
||||
|
||||
## List-Item Extraction
|
||||
|
||||
Extract complex item templates into their own component. Use `@key` for efficient diffing.
|
||||
|
||||
```razor
|
||||
<!-- TaskItem.razor -->
|
||||
<li class="task-item @(Task.IsComplete ? "done" : "")">
|
||||
<input type="checkbox" checked="@Task.IsComplete"
|
||||
@onchange="() => OnToggle.InvokeAsync(Task)" />
|
||||
<span>@Task.Title</span>
|
||||
<button @onclick="() => OnDelete.InvokeAsync(Task)">Delete</button>
|
||||
</li>
|
||||
@code {
|
||||
[Parameter, EditorRequired] public TaskModel Task { get; set; } = default!;
|
||||
[Parameter] public EventCallback<TaskModel> OnToggle { get; set; }
|
||||
[Parameter] public EventCallback<TaskModel> OnDelete { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
```razor
|
||||
<!-- TaskList.razor -->
|
||||
<ul class="task-list">
|
||||
@foreach (var task in Tasks)
|
||||
{
|
||||
<TaskItem @key="task.Id" Task="task"
|
||||
OnToggle="HandleToggle" OnDelete="HandleDelete" />
|
||||
}
|
||||
</ul>
|
||||
```
|
||||
|
||||
## Cascading Context
|
||||
|
||||
Avoid parameter drilling through intermediate components. Cascade a context object or cascade the parent itself.
|
||||
|
||||
```razor
|
||||
<!-- TabSet.razor — cascades itself -->
|
||||
<CascadingValue Value="this" IsFixed="true">
|
||||
<ul class="nav nav-tabs">@ChildContent</ul>
|
||||
</CascadingValue>
|
||||
<div class="tab-body">@ActiveTab?.ChildContent</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
public ITab? ActiveTab { get; private set; }
|
||||
|
||||
public void AddTab(ITab tab) { if (ActiveTab is null) SetActiveTab(tab); }
|
||||
public void SetActiveTab(ITab tab)
|
||||
{
|
||||
if (ActiveTab != tab) { ActiveTab = tab; StateHasChanged(); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```razor
|
||||
<!-- Tab.razor — receives parent via cascading parameter -->
|
||||
@implements ITab
|
||||
<li>
|
||||
<a @onclick="() => ContainerTabSet?.SetActiveTab(this)"
|
||||
class="nav-link @(ContainerTabSet?.ActiveTab == this ? "active" : "")">@Title</a>
|
||||
</li>
|
||||
@code {
|
||||
[CascadingParameter] private TabSet? ContainerTabSet { get; set; }
|
||||
[Parameter] public string? Title { get; set; }
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
protected override void OnInitialized() => ContainerTabSet?.AddTab(this);
|
||||
}
|
||||
```
|
||||
|
||||
- Mark `IsFixed="true"` when the cascaded reference never changes — avoids unnecessary re-renders.
|
||||
- For app-wide values (theme, auth), register via DI: `builder.Services.AddCascadingValue(sp => new ThemeInfo { ... });`
|
||||
@@ -0,0 +1,100 @@
|
||||
# Component Disposal
|
||||
|
||||
Always use `IAsyncDisposable` (not `IDisposable`). Returns `ValueTask` — works for sync and async cleanup.
|
||||
|
||||
## When to Implement
|
||||
|
||||
Implement when component owns: event subscriptions, timers, `CancellationTokenSource`, or JS interop references (`IJSObjectReference`, `DotNetObjectReference<T>`). Otherwise skip disposal.
|
||||
|
||||
## Pattern — Sync Cleanup
|
||||
|
||||
```razor
|
||||
@implements IAsyncDisposable
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
=> Navigation.LocationChanged += HandleLocationChanged;
|
||||
|
||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs e) { }
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
Navigation.LocationChanged -= HandleLocationChanged;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern — JS Interop Cleanup
|
||||
|
||||
```razor
|
||||
@implements IAsyncDisposable
|
||||
@inject IJSRuntime JS
|
||||
|
||||
@code {
|
||||
private IJSObjectReference? module;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
module = await JS.InvokeAsync<IJSObjectReference>("import", "./js/myModule.js");
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (module is not null)
|
||||
{
|
||||
try { await module.DisposeAsync(); }
|
||||
catch (JSDisconnectedException) { } // Circuit already gone
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-pattern — Timer (Don't)
|
||||
|
||||
Prefer `Task.Delay` polling loops (see SKILL.md). If you must use a timer, use `async void` to avoid discarding the `InvokeAsync` task:
|
||||
|
||||
```razor
|
||||
@using System.Timers
|
||||
@implements IAsyncDisposable
|
||||
|
||||
@code {
|
||||
private Timer? timer;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
timer = new Timer(1000);
|
||||
timer.Elapsed += OnTimerElapsed;
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
private async void OnTimerElapsed(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await InvokeAsync(() => { count++; StateHasChanged(); });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DispatchExceptionAsync(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
timer?.Dispose();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Timer.Elapsed` fires on thread-pool thread. `async void` is the only correct handler signature — it awaits `InvokeAsync` and routes errors via `DispatchExceptionAsync`.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Don't** call `StateHasChanged` in `DisposeAsync` — renderer is tearing down.
|
||||
- **Do** null-check fields created in lifecycle methods — `DisposeAsync` may run before `OnInitializedAsync` completes.
|
||||
- **Do** catch `JSDisconnectedException` when disposing JS refs — circuit may be gone.
|
||||
- **Do** unsubscribe all event handlers (`-=`) — subscriptions on long-lived objects leak the component.
|
||||
@@ -0,0 +1,426 @@
|
||||
---
|
||||
license: MIT
|
||||
name: collect-user-input
|
||||
description: Build forms, validate data, and react to user input in Blazor. USE FOR adding forms, search boxes, filter panels, inline editing, data-entry UI, file uploads, validation (annotations or custom), handling form submissions, and binding input controls. Covers EditForm, built-in input components, DataAnnotationsValidator, custom validation, SSR form patterns (SupplyParameterFromForm, FormName, AntiforgeryToken, Enhance), and @bind for simple interactive controls. DO NOT USE for project scaffolding (see create-blazor-project) or prerendering issues (see support-prerendering).
|
||||
---
|
||||
|
||||
# Collect User Input
|
||||
|
||||
## Step 1 — Read the Project's AGENTS.md
|
||||
|
||||
Check `AGENTS.md` for **Interactivity Mode** and **Interactivity Scope**. This determines which form patterns apply:
|
||||
|
||||
| Mode | Form mechanism |
|
||||
|------|---------------|
|
||||
| None (Static SSR) | `EditForm` with `FormName` + `[SupplyParameterFromForm]`. No `@bind`, no `@onchange`. |
|
||||
| Server | `EditForm` with `@bind-Value`. Full interactivity — real-time validation, dynamic UI. |
|
||||
| WebAssembly | Same as Server, but validators needing server data must call APIs. |
|
||||
| Auto | Same as WebAssembly — code must work in both browser and server. |
|
||||
|
||||
| Scope | Impact |
|
||||
|-------|--------|
|
||||
| Global | All forms are interactive. `FormName` only needed when explicitly opting a page to static SSR. |
|
||||
| Per-page | Forms in static pages use `FormName` + `[SupplyParameterFromForm]`. Forms in `@rendermode` pages use `@bind-Value`. |
|
||||
|
||||
## EditForm Setup
|
||||
|
||||
`EditForm` requires **either** `Model` or `EditContext` — never both.
|
||||
|
||||
### Model-based (default)
|
||||
|
||||
```razor
|
||||
<EditForm Model="Employee" OnValidSubmit="HandleSubmit" FormName="employee">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary />
|
||||
|
||||
<label>
|
||||
Name: <InputText @bind-Value="Employee!.Name" />
|
||||
<ValidationMessage For="() => Employee!.Name" />
|
||||
</label>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</EditForm>
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromForm]
|
||||
private EmployeeModel? Employee { get; set; }
|
||||
|
||||
protected override void OnInitialized() => Employee ??= new();
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
// Save Employee
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This single pattern works in **both** SSR and interactive modes:
|
||||
- In SSR: `FormName` identifies the form, `[SupplyParameterFromForm]` binds POST data, `??=` initializes on GET.
|
||||
- In interactive: `@bind-Value` provides two-way binding, `[SupplyParameterFromForm]` is ignored, `FormName` is harmless.
|
||||
|
||||
### EditContext-based (advanced)
|
||||
|
||||
Use when you need programmatic field tracking, dynamic validation rules, or manual `EditContext.Validate()` calls:
|
||||
|
||||
```csharp
|
||||
private EditContext? editContext;
|
||||
private EmployeeModel model = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
editContext = new EditContext(model);
|
||||
}
|
||||
```
|
||||
|
||||
```razor
|
||||
<EditForm EditContext="editContext" OnValidSubmit="HandleSubmit" FormName="employee">
|
||||
```
|
||||
|
||||
## Submit Handlers
|
||||
|
||||
| Handler | Fires when | Use when |
|
||||
|---------|-----------|----------|
|
||||
| `OnValidSubmit` | Validation passes | Standard forms with `DataAnnotationsValidator` |
|
||||
| `OnInvalidSubmit` | Validation fails | Need custom handling for invalid state |
|
||||
| `OnSubmit` | Always — validation is manual | Using `EditContext.Validate()` yourself |
|
||||
|
||||
`OnSubmit` cannot combine with `OnValidSubmit`/`OnInvalidSubmit`.
|
||||
|
||||
## Built-in Input Components
|
||||
|
||||
| Component | Binds to | Notes |
|
||||
|-----------|----------|-------|
|
||||
| `InputText` | `string` | Renders `<input type="text">` |
|
||||
| `InputTextArea` | `string` | Renders `<textarea>` |
|
||||
| `InputNumber<T>` | `int`, `double`, `decimal` | Renders `<input type="number">` |
|
||||
| `InputDate<T>` | `DateTime`, `DateOnly`, `DateTimeOffset` | Renders `<input type="date">` |
|
||||
| `InputCheckbox` | `bool` | Renders `<input type="checkbox">` |
|
||||
| `InputSelect<T>` | `string`, enums, numeric types | Renders `<select>` |
|
||||
| `InputRadioGroup<T>` | `string`, enums, numeric types | Wraps `InputRadio<T>` children |
|
||||
| `InputFile` | `IBrowserFile` | File upload — interactive modes only |
|
||||
|
||||
All input components use `@bind-Value` for binding. Always wrap text in a `<label>` or use `id`/`for` attributes for accessibility.
|
||||
|
||||
### InputSelect with enum values
|
||||
|
||||
```razor
|
||||
<InputSelect @bind-Value="Model!.Status">
|
||||
<option value="">-- Select --</option>
|
||||
@foreach (var value in Enum.GetValues<OrderStatus>())
|
||||
{
|
||||
<option value="@value">@value</option>
|
||||
}
|
||||
</InputSelect>
|
||||
```
|
||||
|
||||
### InputRadioGroup
|
||||
|
||||
```razor
|
||||
<InputRadioGroup @bind-Value="Model!.Priority">
|
||||
@foreach (var p in Enum.GetValues<Priority>())
|
||||
{
|
||||
<label>
|
||||
<InputRadio Value="p" /> @p
|
||||
</label>
|
||||
}
|
||||
</InputRadioGroup>
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
### Data annotations
|
||||
|
||||
Define validation rules on the model:
|
||||
|
||||
```csharp
|
||||
public class EmployeeModel
|
||||
{
|
||||
[Required, StringLength(100)]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[Required, EmailAddress]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[Range(18, 99)]
|
||||
public int Age { get; set; }
|
||||
|
||||
[Required]
|
||||
public string? Department { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
Add `<DataAnnotationsValidator />` inside `EditForm` — without it, annotation attributes are silently ignored.
|
||||
|
||||
Display errors with:
|
||||
- `<ValidationSummary />` — all errors in a list
|
||||
- `<ValidationMessage For="() => Model!.FieldName" />` — per-field inline errors
|
||||
|
||||
### Custom validator component
|
||||
|
||||
For server-round-trip validation (uniqueness checks, business rules):
|
||||
|
||||
```csharp
|
||||
public class CustomValidator : ComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private EditContext? EditContext { get; set; }
|
||||
|
||||
private ValidationMessageStore? messageStore;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
messageStore = new ValidationMessageStore(EditContext!);
|
||||
EditContext!.OnValidationRequested += (s, e) => messageStore.Clear();
|
||||
EditContext!.OnFieldChanged += (s, e) => messageStore.Clear(e.FieldIdentifier);
|
||||
}
|
||||
|
||||
public void DisplayErrors(Dictionary<string, List<string>> errors)
|
||||
{
|
||||
foreach (var (field, messages) in errors)
|
||||
{
|
||||
foreach (var message in messages)
|
||||
{
|
||||
messageStore!.Add(EditContext!.Field(field), message);
|
||||
}
|
||||
}
|
||||
EditContext!.NotifyValidationStateChanged();
|
||||
}
|
||||
|
||||
public void ClearErrors()
|
||||
{
|
||||
messageStore?.Clear();
|
||||
EditContext?.NotifyValidationStateChanged();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage in a form:
|
||||
|
||||
```razor
|
||||
<EditForm Model="Model" OnValidSubmit="HandleSubmit" FormName="register">
|
||||
<DataAnnotationsValidator />
|
||||
<CustomValidator @ref="customValidator" />
|
||||
<ValidationSummary />
|
||||
@* inputs *@
|
||||
</EditForm>
|
||||
|
||||
@code {
|
||||
private CustomValidator? customValidator;
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
var errors = await RegistrationService.ValidateAsync(Model!);
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
customValidator!.DisplayErrors(errors);
|
||||
return;
|
||||
}
|
||||
// proceed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## React to Input Changes (Interactive Only)
|
||||
|
||||
### @bind:after
|
||||
|
||||
Run logic after a bound value changes:
|
||||
|
||||
```razor
|
||||
<InputText @bind-Value="Model!.ZipCode" @bind:after="OnZipCodeChanged" />
|
||||
|
||||
@code {
|
||||
private async Task OnZipCodeChanged()
|
||||
{
|
||||
// Fetch city/state based on new zip code
|
||||
var location = await LocationService.LookupAsync(Model!.ZipCode);
|
||||
Model.City = location?.City;
|
||||
Model.State = location?.State;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @oninput for real-time filtering
|
||||
|
||||
```razor
|
||||
<input type="text" @oninput="OnSearchInput" placeholder="Search..." />
|
||||
|
||||
@code {
|
||||
private string searchTerm = "";
|
||||
private List<Item> filteredItems = new();
|
||||
|
||||
private void OnSearchInput(ChangeEventArgs e)
|
||||
{
|
||||
searchTerm = e.Value?.ToString() ?? "";
|
||||
filteredItems = allItems.Where(i =>
|
||||
i.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SSR-Specific Patterns
|
||||
|
||||
These apply when the form renders in Static SSR (mode = None, or per-page without `@rendermode`).
|
||||
|
||||
### SupplyParameterFromForm
|
||||
|
||||
Binds POST data to a property on form submission:
|
||||
|
||||
```csharp
|
||||
[SupplyParameterFromForm]
|
||||
private ContactModel? Contact { get; set; }
|
||||
|
||||
protected override void OnInitialized() => Contact ??= new();
|
||||
```
|
||||
|
||||
**Critical:** The `??=` in `OnInitialized` is required. On GET the property is null — `??=` creates the model. On POST the framework populates it — `??=` preserves the posted values.
|
||||
|
||||
### FormName — multiple forms on one page
|
||||
|
||||
Each form needs a unique `FormName`:
|
||||
|
||||
```razor
|
||||
<EditForm Model="Search" OnSubmit="DoSearch" FormName="search">...</EditForm>
|
||||
<EditForm Model="Contact" OnValidSubmit="SaveContact" FormName="contact">...</EditForm>
|
||||
```
|
||||
|
||||
Match `[SupplyParameterFromForm]` to its form:
|
||||
|
||||
```csharp
|
||||
[SupplyParameterFromForm(FormName = "search")]
|
||||
private SearchModel? Search { get; set; }
|
||||
|
||||
[SupplyParameterFromForm(FormName = "contact")]
|
||||
private ContactModel? Contact { get; set; }
|
||||
```
|
||||
|
||||
### Enhanced navigation for forms
|
||||
|
||||
Add `Enhance` for SPA-like form submissions without full page reload:
|
||||
|
||||
```razor
|
||||
<EditForm Model="Model" OnValidSubmit="Save" FormName="quick" Enhance>
|
||||
```
|
||||
|
||||
Enhanced forms submit via `fetch`, patch the DOM, and preserve scroll position. The page stays interactive-feeling even in SSR.
|
||||
|
||||
### Plain HTML forms
|
||||
|
||||
When using raw `<form>` instead of `EditForm` in SSR, add the antiforgery token manually:
|
||||
|
||||
```razor
|
||||
<form method="post" @onsubmit="Submit" @formname="raw-form">
|
||||
<AntiforgeryToken />
|
||||
<input name="Model.Name" value="@Model?.Name" />
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
`EditForm` includes the antiforgery token automatically.
|
||||
|
||||
## File Upload
|
||||
|
||||
`InputFile` works in **interactive modes only** — not in Static SSR.
|
||||
|
||||
```razor
|
||||
<InputFile OnChange="OnFileSelected" accept=".pdf,.jpg,.png" />
|
||||
|
||||
@code {
|
||||
private IBrowserFile? selectedFile;
|
||||
|
||||
private async Task OnFileSelected(InputFileChangeEventArgs e)
|
||||
{
|
||||
selectedFile = e.File;
|
||||
|
||||
// Read stream with size limit
|
||||
await using var stream = selectedFile.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
|
||||
// Process stream — save to disk, upload to storage, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Stream size limits:
|
||||
- **Server:** Default ~30 KB SignalR message size. Call `OpenReadStream(maxAllowedSize)` to increase. Large files stream over the circuit.
|
||||
- **WebAssembly:** File is read in the browser. No SignalR limit, but memory constrained.
|
||||
|
||||
For multiple files:
|
||||
|
||||
```razor
|
||||
<InputFile OnChange="OnFilesSelected" multiple />
|
||||
|
||||
@code {
|
||||
private async Task OnFilesSelected(InputFileChangeEventArgs e)
|
||||
{
|
||||
foreach (var file in e.GetMultipleFiles(maxAllowedFiles: 10))
|
||||
{
|
||||
await using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
|
||||
// Process each file
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Prevent Double Submission
|
||||
|
||||
Disable the submit button while processing:
|
||||
|
||||
```razor
|
||||
<button type="submit" disabled="@isSubmitting">
|
||||
@(isSubmitting ? "Saving..." : "Save")
|
||||
</button>
|
||||
|
||||
@code {
|
||||
private bool isSubmitting;
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
isSubmitting = true;
|
||||
try
|
||||
{
|
||||
await SaveService.SaveAsync(Model!);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Validation CSS
|
||||
|
||||
Replace the default `valid`/`invalid` CSS classes:
|
||||
|
||||
```csharp
|
||||
public class BootstrapFieldCssClassProvider : FieldCssClassProvider
|
||||
{
|
||||
public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
|
||||
{
|
||||
var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
|
||||
return editContext.IsModified(fieldIdentifier)
|
||||
? (isValid ? "is-valid" : "is-invalid")
|
||||
: "";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Apply to the form:
|
||||
|
||||
```csharp
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
editContext = new EditContext(model);
|
||||
editContext.SetFieldCssClassProvider(new BootstrapFieldCssClassProvider());
|
||||
}
|
||||
```
|
||||
|
||||
## Don'ts
|
||||
|
||||
- Don't use `@bind` or `@oninput` in Static SSR forms — they require interactivity. Use `[SupplyParameterFromForm]` and `FormName`.
|
||||
- Don't forget `Model ??= new()` in `OnInitialized` — the model is null on GET, populated on POST.
|
||||
- Don't use `OnSubmit` together with `OnValidSubmit`/`OnInvalidSubmit` — they're mutually exclusive.
|
||||
- Don't omit `<DataAnnotationsValidator />` — validation attributes are silently ignored without it.
|
||||
- Don't omit `FormName` in SSR when a page has multiple forms — both forms will fire on any submission.
|
||||
- Don't use `InputFile` in Static SSR — it requires an interactive render mode.
|
||||
- Don't use both `Model` and `EditContext` on an `EditForm` — pick one.
|
||||
- Don't forget `<AntiforgeryToken />` in plain `<form>` elements — the server rejects the POST without it.
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
license: MIT
|
||||
name: configure-auth
|
||||
description: >
|
||||
Add authentication and authorization to a Blazor Web App, accounting for the app's render mode.
|
||||
USE WHEN the user needs [Authorize] on pages, AuthorizeView, role or policy-based access,
|
||||
login/logout Identity pages, or AuthenticationStateProvider.
|
||||
Also USE WHEN auth state is null after WebAssembly loads, SignInManager throws in an interactive
|
||||
component, <NotAuthorized> content never renders in static SSR, or HttpContext.User is null in
|
||||
an interactive component.
|
||||
DO NOT USE for general component authoring (see author-component), for prerendering concerns
|
||||
unrelated to auth (see support-prerendering), or for managing non-auth cascading state
|
||||
(see coordinate-components).
|
||||
---
|
||||
|
||||
# Configure Auth
|
||||
|
||||
## Step 1 — Read AGENTS.md
|
||||
|
||||
Read `AGENTS.md` at the workspace root for the project's interactivity mode and scope before making changes.
|
||||
|
||||
## Step 2 — Register auth services in Program.cs
|
||||
|
||||
```csharp
|
||||
// Program.cs (server project)
|
||||
builder.Services.AddCascadingAuthenticationState();
|
||||
builder.Services.AddAuthorization();
|
||||
```
|
||||
|
||||
For ASP.NET Core Identity add the Identity services:
|
||||
|
||||
```csharp
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = IdentityConstants.ApplicationScheme;
|
||||
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
|
||||
})
|
||||
.AddIdentityCookies();
|
||||
|
||||
builder.Services.AddIdentityCore<ApplicationUser>()
|
||||
.AddRoles<IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddSignInManager()
|
||||
.AddDefaultTokenProviders();
|
||||
```
|
||||
|
||||
## Step 3 — Wire App.razor for auth and render mode
|
||||
|
||||
The `App.razor` component must use `AuthorizeRouteView` and conditionally apply the render mode so that pages excluded from interactive routing render statically.
|
||||
|
||||
```razor
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<HeadOutlet @rendermode="RenderModeForPage" />
|
||||
</head>
|
||||
<body>
|
||||
<Routes @rendermode="RenderModeForPage" />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
public HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
private IComponentRenderMode? RenderModeForPage =>
|
||||
HttpContext.AcceptsInteractiveRouting()
|
||||
? InteractiveServer // replace with the app's render mode
|
||||
: null;
|
||||
}
|
||||
```
|
||||
|
||||
In `Routes.razor` (or wherever the router lives), use `AuthorizeRouteView`:
|
||||
|
||||
```razor
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="routeData"
|
||||
DefaultLayout="typeof(Layout.MainLayout)">
|
||||
<NotAuthorized>
|
||||
@if (context.User.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
<RedirectToLogin />
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>You are not authorized to access this resource.</p>
|
||||
}
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
```
|
||||
|
||||
## Step 4 — Protect pages and components
|
||||
|
||||
### [Authorize] attribute on pages
|
||||
|
||||
```razor
|
||||
@page "/admin"
|
||||
@attribute [Authorize]
|
||||
```
|
||||
|
||||
With roles or policies:
|
||||
|
||||
```razor
|
||||
@attribute [Authorize(Roles = "Admin")]
|
||||
@attribute [Authorize(Policy = "RequireManager")]
|
||||
```
|
||||
|
||||
### AuthorizeView for conditional UI
|
||||
|
||||
```razor
|
||||
<AuthorizeView>
|
||||
<Authorized>Welcome, @context.User.Identity?.Name!</Authorized>
|
||||
<NotAuthorized><a href="Account/Login">Log in</a></NotAuthorized>
|
||||
</AuthorizeView>
|
||||
```
|
||||
|
||||
Role/policy variants:
|
||||
|
||||
```razor
|
||||
<AuthorizeView Roles="Admin,Manager">
|
||||
<Authorized>Admin content here</Authorized>
|
||||
</AuthorizeView>
|
||||
```
|
||||
|
||||
### Access auth state in code
|
||||
|
||||
```csharp
|
||||
[CascadingParameter]
|
||||
private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthState is not null)
|
||||
{
|
||||
var state = await AuthState;
|
||||
var isAdmin = state.User.IsInRole("Admin");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5 — Identity pages must stay static SSR
|
||||
|
||||
`SignInManager` and `UserManager` use `HttpContext` internally and **throw in interactive components**. Identity pages (login, register, manage) must render as static SSR.
|
||||
|
||||
In a **globally interactive** app, mark every Identity page:
|
||||
|
||||
```razor
|
||||
@page "/Account/Login"
|
||||
@attribute [ExcludeFromInteractiveRouting]
|
||||
```
|
||||
|
||||
This forces a full-page navigation (exits the interactive circuit) so the page renders through the static SSR pipeline with a real `HttpContext`.
|
||||
|
||||
`App.razor` must use `AcceptsInteractiveRouting()` (Step 3) to return `null` for these pages — otherwise the framework still tries to render them interactively.
|
||||
|
||||
In a **per-page** app, Identity pages are static by default (no `@rendermode` directive), so `[ExcludeFromInteractiveRouting]` is not needed.
|
||||
|
||||
## Step 6 — Auth state in WebAssembly / Auto mode
|
||||
|
||||
WebAssembly components run in the browser and have no `HttpContext`. Auth state must be serialized from the server during prerendering and deserialized on the client.
|
||||
|
||||
**Server `Program.cs`:**
|
||||
|
||||
```csharp
|
||||
builder.Services.AddAuthenticationStateSerialization();
|
||||
```
|
||||
|
||||
**Client `.Client/Program.cs`:**
|
||||
|
||||
```csharp
|
||||
builder.Services.AddAuthenticationStateDeserialization();
|
||||
```
|
||||
|
||||
Without these calls, `Task<AuthenticationState>` resolves to an anonymous user after WebAssembly takes over from prerendering.
|
||||
|
||||
`AddAuthenticationStateSerialization` accepts options to include role and claim data:
|
||||
|
||||
```csharp
|
||||
builder.Services.AddAuthenticationStateSerialization(options =>
|
||||
options.SerializeAllClaims = true);
|
||||
```
|
||||
|
||||
## Render Mode × Auth Matrix
|
||||
|
||||
| Render mode | HttpContext.User | SignInManager | Auth state source | Key requirement |
|
||||
|---|---|---|---|---|
|
||||
| Static SSR | Available | Works | Server pipeline | Use middleware for redirects, `<NotAuthorized>` does NOT render |
|
||||
| Server (interactive) | NOT available | Throws | `CascadingAuthenticationState` | Use `[Authorize]` + `AuthorizeView`, not `HttpContext` |
|
||||
| WebAssembly | NOT available | Throws | Serialized from server | `AddAuthenticationStateSerialization` / `Deserialization` |
|
||||
| Auto | NOT available after WASM | Throws | Serialized from server | Same as WebAssembly; register in **both** Program.cs files |
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Symptom | Fix |
|
||||
|---------|---------|-----|
|
||||
| Using `HttpContext.User` in interactive component | Null or stale claims | Use `[CascadingParameter] Task<AuthenticationState>` |
|
||||
| `SignInManager` in interactive component | `InvalidOperationException` | Move to static SSR page with `[ExcludeFromInteractiveRouting]` |
|
||||
| Missing `AddAuthenticationStateSerialization` | Anonymous user after WASM loads | Add to server Program.cs; add `Deserialization` to client Program.cs |
|
||||
| `<NotAuthorized>` in static SSR layout | Content never shown | Static SSR uses middleware pipeline; redirect via `LoginPath` or `RedirectToLogin` component |
|
||||
| Global interactivity without `AcceptsInteractiveRouting` | Identity pages crash | Add `AcceptsInteractiveRouting()` check in App.razor (Step 3) |
|
||||
| Missing `AddCascadingAuthenticationState()` | `Task<AuthenticationState>` is null | Register in Program.cs (Step 2) |
|
||||
@@ -0,0 +1,235 @@
|
||||
---
|
||||
license: MIT
|
||||
name: coordinate-components
|
||||
description: >
|
||||
Share state between components that don't have a direct parent-child parameter relationship,
|
||||
using cascading values, scoped services with change events, or CascadingValueSource via DI.
|
||||
USE WHEN the user needs a CascadingParameter or CascadingValue that works across render mode
|
||||
boundaries, a shopping cart or notification count accessible from multiple pages, a theme or
|
||||
user preference cascaded app-wide, or when components in different parts of the tree must
|
||||
react when shared data changes. Also USE WHEN cascading values aren't reaching interactive
|
||||
children in per-page interactivity mode, or when the user needs to understand scoped vs
|
||||
singleton service lifetime for state on Blazor Server.
|
||||
DO NOT USE for direct parent-child parameter passing or EventCallback (see author-component),
|
||||
for persisting state across prerender-to-interactive transitions (see support-prerendering),
|
||||
or for service abstractions for data fetching in Auto/WebAssembly (see fetch-and-send-data).
|
||||
---
|
||||
|
||||
# Coordinate Components
|
||||
|
||||
## Step 1 — Read AGENTS.md
|
||||
|
||||
Read `AGENTS.md` at the workspace root to learn the project's conventions before making changes.
|
||||
|
||||
## Step 2 — Decide the scope
|
||||
|
||||
| Need | Mechanism | When to use |
|
||||
|------|-----------|-------------|
|
||||
| Subtree (same render mode) | `CascadingValue` component | Theme, layout config within a layout |
|
||||
| App-wide (all render modes) | `CascadingValueSource<T>` via DI | Current user, feature flags, theme shared globally |
|
||||
| Mutable shared state within a circuit | Scoped service + `Action` event | Shopping cart, notification count, selected filters |
|
||||
|
||||
For parent→child one level: use `[Parameter]` / `EventCallback` (see `author-component` skill).
|
||||
For persisting state across prerender→interactive: see `support-prerendering` skill.
|
||||
|
||||
## Workflow (quick reference)
|
||||
|
||||
1. Choose the mechanism from the table in Step 2
|
||||
2. If crossing render mode boundaries → use `CascadingValueSource<T>` (Step 4)
|
||||
3. Register in `Program.cs` with `AddCascadingValue(...)` and `isFixed: false`
|
||||
4. Consume via `[CascadingParameter]` in child components
|
||||
5. Update via `NotifyChangedAsync(newValue)` — never page reload
|
||||
6. For additional mutable state within a circuit → add scoped service (Step 5)
|
||||
7. Wrap any `StateHasChanged` from background threads in `InvokeAsync`
|
||||
8. Implement `IDisposable` — dispose timers, cancel tokens, unsubscribe events
|
||||
|
||||
## Step 3 — CascadingValue for subtree state
|
||||
|
||||
Wrap a subtree with `<CascadingValue>` to flow data to all descendants without passing it through every intermediate component.
|
||||
|
||||
```razor
|
||||
@* In a layout or parent component *@
|
||||
<CascadingValue Value="theme">
|
||||
@Body
|
||||
</CascadingValue>
|
||||
|
||||
@code {
|
||||
private ThemeInfo theme = new() { ButtonClass = "btn-primary" };
|
||||
}
|
||||
```
|
||||
|
||||
Consume in any descendant:
|
||||
|
||||
```csharp
|
||||
[CascadingParameter]
|
||||
private ThemeInfo? Theme { get; set; }
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Matched by **type**, not name. To cascade multiple values of the same type, add `Name`:
|
||||
```razor
|
||||
<CascadingValue Value="primary" Name="PrimaryTheme">...</CascadingValue>
|
||||
```
|
||||
```csharp
|
||||
[CascadingParameter(Name = "PrimaryTheme")]
|
||||
private ThemeInfo? Primary { get; set; }
|
||||
```
|
||||
- Set `IsFixed="true"` when the value never changes — avoids subscription overhead.
|
||||
- **Does NOT cross render mode boundaries.** A `<CascadingValue>` in a static SSR parent is invisible to interactive children. See Step 6.
|
||||
|
||||
## Step 4 — CascadingValueSource<T> for app-wide state
|
||||
|
||||
Register a `CascadingValueSource<T>` in DI when the value must be available to **all components regardless of render mode**.
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
builder.Services.AddCascadingValue(sp =>
|
||||
{
|
||||
var theme = new ThemeInfo { ButtonClass = "btn-primary" };
|
||||
return new CascadingValueSource<ThemeInfo>(theme, isFixed: false);
|
||||
});
|
||||
```
|
||||
|
||||
Consume identically to Step 3:
|
||||
|
||||
```csharp
|
||||
[CascadingParameter]
|
||||
private ThemeInfo? Theme { get; set; }
|
||||
```
|
||||
|
||||
**To update and notify subscribers**, either mutate the existing object or replace it:
|
||||
|
||||
```razor
|
||||
@* Component that changes the theme *@
|
||||
@inject CascadingValueSource<ThemeInfo> ThemeSource
|
||||
|
||||
<button @onclick="ToggleDarkMode">Toggle theme</button>
|
||||
|
||||
@code {
|
||||
private bool isDark;
|
||||
|
||||
private async Task ToggleDarkMode()
|
||||
{
|
||||
isDark = !isDark;
|
||||
// Replace the value entirely:
|
||||
var newTheme = new ThemeInfo { ButtonClass = isDark ? "btn-dark" : "btn-primary" };
|
||||
await ThemeSource.NotifyChangedAsync(newTheme);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`NotifyChangedAsync()` (no argument) also works — mutate the object and then call it. `NotifyChangedAsync(newValue)` replaces the value and notifies in one step.
|
||||
|
||||
**Update protocol:** Whenever shared state changes, the component that changes it MUST inject `CascadingValueSource<T>` and call `NotifyChangedAsync()`. This is the only mechanism that triggers re-rendering in all `[CascadingParameter]` subscribers. Without this call, no subscribers update. Do not use `NavigationManager.Refresh()` or page reloads as a substitute.
|
||||
|
||||
**Rules:**
|
||||
- `isFixed: false` enables change notifications. `isFixed: true` is better for truly static values (feature flags).
|
||||
- **Crosses render mode boundaries** — works for per-page interactivity, global interactivity, and WebAssembly. Key advantage over `<CascadingValue>`.
|
||||
- Keep cascaded types **granular**. Every `NotifyChangedAsync` re-renders ALL subscribers regardless of which property changed. Don't put all app state into one cascaded type.
|
||||
- For Auto/WebAssembly apps, register in **both** server and `.Client` `Program.cs`. The type must be in a shared assembly.
|
||||
|
||||
## Step 5 — Scoped state service with change events
|
||||
|
||||
For mutable shared state that multiple components read **and write** (shopping cart, notification count, filters), use a scoped service with an event for change notification.
|
||||
|
||||
**Define the service:**
|
||||
|
||||
```csharp
|
||||
public class CartState
|
||||
{
|
||||
private readonly List<CartItem> _items = [];
|
||||
|
||||
public IReadOnlyList<CartItem> Items => _items;
|
||||
public int Count => _items.Count;
|
||||
|
||||
public event Action? OnChange;
|
||||
|
||||
public void Add(CartItem item)
|
||||
{
|
||||
_items.Add(item);
|
||||
OnChange?.Invoke();
|
||||
}
|
||||
|
||||
public void Remove(CartItem item)
|
||||
{
|
||||
_items.Remove(item);
|
||||
OnChange?.Invoke();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Register as scoped:**
|
||||
|
||||
```csharp
|
||||
builder.Services.AddScoped<CartState>();
|
||||
```
|
||||
|
||||
**Subscribe in components:**
|
||||
|
||||
```razor
|
||||
@inject CartState Cart
|
||||
@implements IDisposable
|
||||
|
||||
<span class="badge">@Cart.Count</span>
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Cart.OnChange += StateHasChanged;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cart.OnChange -= StateHasChanged;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The simple `Action OnChange` pattern works when the event fires from the Blazor sync context (button click → `Cart.Add(…)`). If the event fires from **outside** the sync context (timer, background task, SignalR hub), wrap in `InvokeAsync`:
|
||||
|
||||
```csharp
|
||||
private Action? _handler;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_handler = () => InvokeAsync(StateHasChanged);
|
||||
Cart.OnChange += _handler;
|
||||
}
|
||||
|
||||
public void Dispose() => Cart.OnChange -= _handler;
|
||||
```
|
||||
|
||||
Store the delegate in a field so you can unsubscribe the exact same instance.
|
||||
|
||||
## Step 6 — Render mode and service lifetime rules
|
||||
|
||||
### Cascading values don't cross render mode boundaries
|
||||
|
||||
A `<CascadingValue>` placed in a static SSR layout (`MainLayout.razor` when the layout renders statically) will **not** reach interactive children. The interactive component sees `null` for the cascading parameter.
|
||||
|
||||
**Fix:** Use `CascadingValueSource<T>` registered in DI (Step 4) or a scoped service (Step 5). Both cross boundaries because DI services are resolved per-circuit, not from the component tree.
|
||||
|
||||
### Service lifetime on Server vs WebAssembly
|
||||
|
||||
| Lifetime | Server | WebAssembly |
|
||||
|----------|--------|-------------|
|
||||
| **Scoped** | Per circuit (per user connection) | Per browser tab |
|
||||
| **Singleton** | Shared across ALL users | Per browser tab (safe) |
|
||||
| **Transient** | New instance per injection | New instance per injection |
|
||||
|
||||
On Server, **never store user-specific state in a singleton** — every user's circuit shares the same singleton. One user's cart leaks into another's. Use `AddScoped<T>()`.
|
||||
|
||||
On WebAssembly, singletons are per-tab and safe. But code meant for **both** Server and WebAssembly (Auto mode) must use scoped.
|
||||
|
||||
### Auto/WebAssembly with prerendering
|
||||
|
||||
State services must be defined in the `.Client` project or a shared assembly — they cannot reference server-only types. Register the service in both `Program.cs` files. State created during prerender does not survive the switch to the interactive runtime. Use the `support-prerendering` skill's `[PersistentState]` pattern to carry state across.
|
||||
|
||||
## Don'ts
|
||||
|
||||
- **Don't use a singleton for per-user state on Server** — all circuits share it, leaking state between users.
|
||||
- **Don't put all app state into one cascaded object** — `NotifyChangedAsync` re-renders ALL subscribers on every change. Separate concerns into distinct types (`ThemeState`, `CartState`, `UserPreferences`).
|
||||
- **Don't forget to unsubscribe** — omitting `Dispose` on event subscriptions causes memory leaks that grow per-circuit.
|
||||
- **Don't use `<CascadingValue>` in a static layout expecting it to reach interactive children** — it won't cross render mode boundaries. Use DI-registered `CascadingValueSource<T>` or scoped services.
|
||||
- **Don't use `NavigationManager.Refresh(forceReload: true)` to propagate cascading value changes** — this destroys the circuit and forces a full page reload. Instead, inject `CascadingValueSource<T>` and call `NotifyChangedAsync(newValue)` to push updates to all `[CascadingParameter]` subscribers without a page reload.
|
||||
- **Don't call `StateHasChanged` from a non-Blazor thread** — wrap in `InvokeAsync`. The framework throws `InvalidOperationException: The current thread is not associated with the Dispatcher`.
|
||||
@@ -0,0 +1,293 @@
|
||||
---
|
||||
license: MIT
|
||||
name: create-blazor-project
|
||||
description: >
|
||||
Create a new ASP.NET Core web application or web site using Blazor.
|
||||
USE FOR: creating a new Blazor web app, scaffolding a new web project,
|
||||
starting a new web site, choosing render modes (Static SSR, Interactive Server,
|
||||
Interactive WebAssembly, Auto), running dotnet new blazor with the right options,
|
||||
setting up initial project structure.
|
||||
DO NOT USE FOR: adding features to existing projects, changing how an existing
|
||||
app renders, or component authoring (use author-component).
|
||||
---
|
||||
|
||||
# Create a Blazor Web App
|
||||
|
||||
## Before You Start — Gather Requirements
|
||||
|
||||
If the user's request doesn't make the following clear, ask before scaffolding:
|
||||
|
||||
1. **What does the app do?** List the main screens/features (e.g., "product catalog with search and shopping cart").
|
||||
2. **What kind of interactivity is needed?** Displaying data and forms? Real-time updates? Offline support? Rich drag-and-drop UI?
|
||||
3. **Deployment environment?** Internet-facing? Intranet? Mobile users on slow connections?
|
||||
4. **Authentication needed?** Anonymous? Individual accounts? Organizational (Azure AD)?
|
||||
|
||||
## Pick the Right Interactivity Level
|
||||
|
||||
Blazor render modes are a progression scale. Start at the simplest level that satisfies the requirements and only move up when there's a concrete reason.
|
||||
|
||||
```
|
||||
Static SSR ──→ SSR + Enhanced Nav ──→ Interactive Server ──→ Interactive WebAssembly
|
||||
simplest most complex
|
||||
```
|
||||
|
||||
### Decision Rules
|
||||
|
||||
| If the app needs... | Use | Why |
|
||||
|---|---|---|
|
||||
| Display data, simple forms, links between pages | **Static SSR** (`-int None`) | No JS runtime, no circuit, no WebAssembly download. Forms work via HTML POST. Enhanced navigation makes it feel snappy. |
|
||||
| Everything above + a few components with client-side behavior (live search, real-time updates, complex form wizards) | **Interactive Server, per-page** (`-int Server`) | Only the components that need interactivity opt in with `@rendermode`. The rest stays static. Server-side execution, full .NET access, no API layer needed. |
|
||||
| Most pages need rich interactivity (dashboards, drag-and-drop, chat) | **Interactive Server, global** (`-int Server -ai`) | Every component is interactive by default. Consistent UX, simpler mental model. Trade-off: every user holds a SignalR circuit on the server. |
|
||||
| Network latency is a problem, users are on mobile/poor connections, or the app must work offline | **Interactive WebAssembly** (`-int WebAssembly`) | Code runs in the browser. Eliminates round-trip latency but requires a `.Client` project, API layer for data access, and downloads the .NET runtime to the browser on first visit. For offline support, enable PWA: add a service worker and manifest after scaffolding (not included in the template by default). |
|
||||
| Fast initial load (Server) + low latency after (WebAssembly) | **Interactive Auto** (`-int Auto`) | First visit uses Server; subsequent visits use cached WebAssembly runtime. Most complex setup — see Auto constraints below. Only choose when both Server and WebAssembly constraints apply. |
|
||||
|
||||
**Default recommendation:** Start with `-int Server` (per-page). It covers the vast majority of apps. Upgrade to global or WebAssembly only when a specific requirement demands it.
|
||||
|
||||
### Auto Mode Constraints
|
||||
|
||||
Auto mode means your component code runs on the server first, then in the browser on subsequent visits. This creates real constraints:
|
||||
|
||||
- **All interactive components must live in the `.Client` project** — same as WebAssembly.
|
||||
- **No direct server access** from interactive components — no EF `DbContext`, no file system, no server-only services. All data access must go through HTTP APIs.
|
||||
- **Both `Program.cs` files must register matching services** — the server and client DI containers must both provide implementations for any service an interactive component injects.
|
||||
- **Code must not assume its execution environment** — no `HttpContext` access, no browser-only APIs without `RendererInfo` guards.
|
||||
- **Test in both modes** — a component that works on Server during development may break on WebAssembly in production (second visit). Test both paths.
|
||||
|
||||
### Don'ts
|
||||
|
||||
- Don't pick WebAssembly "because it's cool" — it adds a `.Client` project, forces API-mediated data access, and downloads ~10MB to the browser on first visit.
|
||||
- Don't pick Auto unless you can articulate why Server alone and WebAssembly alone are both insufficient.
|
||||
- Don't pick global interactivity for apps where most pages are read-only content — per-page keeps the static pages fast and reduces server memory.
|
||||
|
||||
## Scaffold the Project
|
||||
|
||||
### Static SSR Only (display data + simple forms)
|
||||
|
||||
```shell
|
||||
dotnet new blazor -o {AppName} -int None
|
||||
```
|
||||
|
||||
No interactive runtime. Enhanced navigation enabled by default via `blazor.web.js`.
|
||||
|
||||
### Interactive Server, Per-Page (recommended default)
|
||||
|
||||
```shell
|
||||
dotnet new blazor -o {AppName} -int Server
|
||||
```
|
||||
|
||||
Pages are static by default. Add `@rendermode InteractiveServer` to components that need interactivity.
|
||||
|
||||
### Interactive Server, Global
|
||||
|
||||
```shell
|
||||
dotnet new blazor -o {AppName} -int Server -ai
|
||||
```
|
||||
|
||||
All pages interactive via `<Routes @rendermode="InteractiveServer" />` in `App.razor`.
|
||||
|
||||
### Interactive WebAssembly, Per-Page
|
||||
|
||||
```shell
|
||||
dotnet new blazor -o {AppName} -int WebAssembly
|
||||
```
|
||||
|
||||
Creates `{AppName}` (server) and `{AppName}.Client` (WebAssembly) projects. Interactive components must live in `.Client`.
|
||||
|
||||
### Interactive WebAssembly, Global
|
||||
|
||||
```shell
|
||||
dotnet new blazor -o {AppName} -int WebAssembly -ai
|
||||
```
|
||||
|
||||
### Interactive Auto, Per-Page
|
||||
|
||||
```shell
|
||||
dotnet new blazor -o {AppName} -int Auto
|
||||
```
|
||||
|
||||
### Interactive Auto, Global
|
||||
|
||||
```shell
|
||||
dotnet new blazor -o {AppName} -int Auto -ai
|
||||
```
|
||||
|
||||
### With Authentication
|
||||
|
||||
Append `-au Individual` to any command above:
|
||||
|
||||
```shell
|
||||
dotnet new blazor -o {AppName} -int Server -au Individual
|
||||
```
|
||||
|
||||
`-au Individual` scaffolds ASP.NET Core Identity with SQLite (CLI) or SQL Server (Visual Studio). Identity pages are always static SSR — they do not use interactive render modes.
|
||||
|
||||
The `blazor` template only supports `-au Individual`. For organizational auth (Microsoft Entra ID, Azure AD B2C), scaffold with `-au Individual` first, then replace the Identity provider with `Microsoft.Identity.Web` / OIDC middleware and configure the tenant in `appsettings.json`.
|
||||
|
||||
## What the Template Creates
|
||||
|
||||
### Single project (Static SSR, Server)
|
||||
|
||||
```
|
||||
{AppName}/
|
||||
├── Components/
|
||||
│ ├── App.razor # Root component — sets <HeadOutlet> and <Routes>
|
||||
│ ├── Routes.razor # Wraps <Router> with route discovery
|
||||
│ ├── Layout/
|
||||
│ │ ├── MainLayout.razor # App shell with nav, header, footer
|
||||
│ │ └── MainLayout.razor.css
|
||||
│ └── Pages/
|
||||
│ └── Home.razor # @page "/" — first page
|
||||
├── Program.cs # Service registration and middleware
|
||||
├── wwwroot/ # Static files (CSS, images)
|
||||
└── {AppName}.csproj
|
||||
```
|
||||
|
||||
### Two projects (WebAssembly, Auto)
|
||||
|
||||
```
|
||||
{AppName}/ # Server project — hosts the app
|
||||
├── Components/ # Server-only components (static SSR pages, layouts)
|
||||
│ ├── App.razor
|
||||
│ ├── Routes.razor
|
||||
│ └── Layout/
|
||||
├── Program.cs # Server Program.cs
|
||||
└── {AppName}.Client/ # Client project — WebAssembly components
|
||||
├── Pages/ # Interactive components go HERE
|
||||
├── Program.cs # Client Program.cs
|
||||
└── _Imports.razor
|
||||
```
|
||||
|
||||
**Rule:** Components using `InteractiveWebAssembly` or `InteractiveAuto` must live in the `.Client` project. They can reference shared code but cannot reference server-only types (EF `DbContext`, server-side services).
|
||||
|
||||
## Program.cs Wiring
|
||||
|
||||
The template generates the correct `Program.cs` for the chosen mode. Verify these registrations match your intent:
|
||||
|
||||
### Static SSR Only
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
builder.Services.AddRazorComponents();
|
||||
|
||||
// ...
|
||||
|
||||
app.MapRazorComponents<App>();
|
||||
```
|
||||
|
||||
### Server (per-page or global)
|
||||
|
||||
```csharp
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
// ...
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
```
|
||||
|
||||
### WebAssembly (per-page or global)
|
||||
|
||||
```csharp
|
||||
// Server Program.cs
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveWebAssemblyComponents();
|
||||
|
||||
// ...
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveWebAssemblyRenderMode()
|
||||
.AddAdditionalAssemblies(typeof({AppName}.Client._Imports).Assembly);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Client Program.cs
|
||||
builder.Services.AddAuthorizationCore();
|
||||
// Register HttpClient, other client-side services
|
||||
```
|
||||
|
||||
## Create Project AGENTS.md
|
||||
|
||||
After scaffolding, create an `AGENTS.md` file in the project root (next to the `.csproj`). For two-project setups, put it in the server project root.
|
||||
|
||||
Pick the matching template from `assets/agents-md/` based on the chosen mode:
|
||||
|
||||
| Mode | Template file |
|
||||
|------|--------------|
|
||||
| Static SSR (`-int None`) | `assets/agents-md/ssr-none.md` |
|
||||
| Server, per-page (`-int Server`) | `assets/agents-md/server-per-page.md` |
|
||||
| Server, global (`-int Server -ai`) | `assets/agents-md/server-global.md` |
|
||||
| WebAssembly, per-page (`-int WebAssembly`) | `assets/agents-md/webassembly-per-page.md` |
|
||||
| WebAssembly, global (`-int WebAssembly -ai`) | `assets/agents-md/webassembly-global.md` |
|
||||
| Auto, per-page (`-int Auto`) | `assets/agents-md/auto-per-page.md` |
|
||||
| Auto, global (`-int Auto -ai`) | `assets/agents-md/auto-global.md` |
|
||||
|
||||
Copy the template contents into the project's `AGENTS.md` and replace every `{AppName}` with the actual project name. If auth was scaffolded (`-au Individual`), add an `## Authentication` section noting that ASP.NET Core Identity is configured and that Identity pages under `Components/Account/` are always static SSR — do not add `@rendermode` to them.
|
||||
|
||||
**After scaffolding the project and creating AGENTS.md, continue implementing the features the user requested.** Remove default template pages (Counter, Weather) and replace them with the actual application pages.
|
||||
|
||||
### Auto (per-page or global)
|
||||
|
||||
```csharp
|
||||
// Server Program.cs
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents()
|
||||
.AddInteractiveWebAssemblyComponents();
|
||||
|
||||
// ...
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AddInteractiveWebAssemblyRenderMode()
|
||||
.AddAdditionalAssemblies(typeof({AppName}.Client._Imports).Assembly);
|
||||
```
|
||||
|
||||
## App.razor — Global vs Per-Page
|
||||
|
||||
The difference between global and per-page interactivity is entirely in `App.razor`:
|
||||
|
||||
### Per-page (default)
|
||||
|
||||
```razor
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
<body>
|
||||
<Routes />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
No `@rendermode` on `<Routes>` or `<HeadOutlet>`. Individual pages opt in.
|
||||
|
||||
### Global
|
||||
|
||||
```razor
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<HeadOutlet @rendermode="InteractiveServer" />
|
||||
</head>
|
||||
<body>
|
||||
<Routes @rendermode="InteractiveServer" />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Replace `InteractiveServer` with `InteractiveWebAssembly` or `InteractiveAuto` as appropriate.
|
||||
|
||||
## After Scaffolding
|
||||
|
||||
1. **Verify it builds:** `dotnet build`
|
||||
2. **Run it:** `dotnet run` (in the server project if two-project setup)
|
||||
3. **Add your first page:** Create a `.razor` file in `Components/Pages/` (server project) or `Pages/` (`.Client` project for WebAssembly components)
|
||||
|
||||
## Don'ts
|
||||
|
||||
- Don't use `dotnet new blazorwasm` — that creates a standalone WebAssembly SPA without server-side rendering. Use the `blazor` template with `-int WebAssembly` instead.
|
||||
- Don't manually add `AddInteractiveServerComponents()` to a project created with `-int None` and expect it to work — you also need the `@rendermode` directives and potentially `App.razor` changes. Re-scaffold if the mode needs to change fundamentally.
|
||||
- Don't put WebAssembly-targeted components in the server project — they'll work during prerender but fail after handoff.
|
||||
@@ -0,0 +1,47 @@
|
||||
# {AppName}
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| **Interactivity Mode** | Auto |
|
||||
| **Interactivity Scope** | Global |
|
||||
|
||||
## Rendering configuration
|
||||
This project uses global Interactive Auto with prerendering.
|
||||
Created with `dotnet new blazor -int Auto -ai`.
|
||||
|
||||
On a user's first visit, components render via Interactive Server (SignalR). On subsequent visits the cached WebAssembly runtime takes over and interactions run entirely in the browser.
|
||||
|
||||
## Project structure
|
||||
- **{AppName}** (server): Hosts the Blazor app, serves static files, API endpoints.
|
||||
- **{AppName}.Client** (WebAssembly): All interactive UI components. Run on server first, then browser.
|
||||
|
||||
## Adding new components
|
||||
- Interactive components MUST go in the `.Client` project, not the server project.
|
||||
- New pages go in `{AppName}.Client/Pages/`.
|
||||
- All pages are already interactive (global mode). No need to add `@rendermode` to individual components.
|
||||
- Server-only static components (e.g., error pages) belong in the server `Components/` folder.
|
||||
|
||||
## Data access
|
||||
Interactive components cannot access the database directly. Use this pattern:
|
||||
1. Define an interface in the `.Client` project (e.g., `IDataService`).
|
||||
2. In the `.Client` project, implement it using `HttpClient` to call server APIs.
|
||||
3. In the server project, implement it using direct data access (EF Core DbContext, etc.).
|
||||
4. Register the client implementation in the client `Program.cs` and the server implementation in the server `Program.cs`.
|
||||
5. Expose server data through minimal API endpoints (e.g., `app.MapGet(...)`) that the client implementation calls.
|
||||
6. If the page requires authorization, apply the same auth policy to both the Blazor page (`@attribute [Authorize]`) and the minimal API endpoint (`.RequireAuthorization()`).
|
||||
|
||||
## Service registration
|
||||
- Both server and client `Program.cs` must register matching services for any DI used by interactive components.
|
||||
- Server-only services (EF Core, Identity) stay in the server `Program.cs` only.
|
||||
|
||||
## Environment constraints
|
||||
- Code must work in both server and browser execution environments.
|
||||
- Do not use `HttpContext` or browser-only JS APIs without `RendererInfo` guards.
|
||||
- The .NET runtime (~10 MB) is downloaded to the browser on first visit and cached — subsequent visits use WebAssembly.
|
||||
|
||||
## Don'ts
|
||||
- Don't put interactive components in the server project — they work during prerender but fail after WebAssembly handoff.
|
||||
- Don't inject `DbContext` or server-only services in `.Client` project components — use HTTP APIs instead.
|
||||
- Don't assume execution environment — the same component runs on Server first, then WebAssembly later. Test both.
|
||||
- Don't add `@rendermode InteractiveAuto` to pages — global interactivity is already configured in `App.razor`.
|
||||
- Don't add `@rendermode` to Identity/Account pages if auth is configured — they must stay static SSR.
|
||||
@@ -0,0 +1,48 @@
|
||||
# {AppName}
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| **Interactivity Mode** | Auto |
|
||||
| **Interactivity Scope** | Per-page |
|
||||
|
||||
## Rendering configuration
|
||||
This project uses per-page Interactive Auto with prerendering.
|
||||
Created with `dotnet new blazor -int Auto`.
|
||||
|
||||
Pages are static SSR by default. Components that add `@rendermode InteractiveAuto` use Server on first visit, then WebAssembly on subsequent visits once the runtime is cached.
|
||||
|
||||
## Project structure
|
||||
- **{AppName}** (server): Hosts the Blazor app, serves static files, API endpoints. Static SSR pages and layouts live here.
|
||||
- **{AppName}.Client** (WebAssembly): Interactive components that run on server first, then browser.
|
||||
|
||||
## Adding new components
|
||||
- Interactive components MUST go in the `.Client` project, not the server project.
|
||||
- New pages in the server `Components/Pages/` are static SSR by default.
|
||||
- Only add `@rendermode InteractiveAuto` to components that need client-side interactivity.
|
||||
- Static pages can use standard HTML forms with `[SupplyParameterFromForm]` — no interactivity needed.
|
||||
|
||||
## Data access
|
||||
Interactive components cannot access the database directly. Use this pattern:
|
||||
1. Define an interface in the `.Client` project (e.g., `IDataService`).
|
||||
2. In the `.Client` project, implement it using `HttpClient` to call server APIs.
|
||||
3. In the server project, implement it using direct data access (EF Core DbContext, etc.).
|
||||
4. Register the client implementation in the client `Program.cs` and the server implementation in the server `Program.cs`.
|
||||
5. Expose server data through minimal API endpoints (e.g., `app.MapGet(...)`) that the client implementation calls.
|
||||
6. If the page requires authorization, apply the same auth policy to both the Blazor page (`@attribute [Authorize]`) and the minimal API endpoint (`.RequireAuthorization()`).
|
||||
|
||||
## Service registration
|
||||
- Both server and client `Program.cs` must register matching services for any DI used by interactive components.
|
||||
- Server-only services (EF Core, Identity) stay in the server `Program.cs` only.
|
||||
|
||||
## Environment constraints
|
||||
- Code must work in both server and browser execution environments.
|
||||
- Do not use `HttpContext` or browser-only JS APIs without `RendererInfo` guards.
|
||||
- The .NET runtime (~10 MB) is downloaded to the browser on first visit and cached — subsequent visits use WebAssembly.
|
||||
- Static SSR pages in the server project have full server access.
|
||||
|
||||
## Don'ts
|
||||
- Don't put interactive components in the server project — they work during prerender but fail after WebAssembly handoff.
|
||||
- Don't inject `DbContext` or server-only services in `.Client` project components — use HTTP APIs instead.
|
||||
- Don't assume execution environment — the same component runs on Server first, then WebAssembly later. Test both.
|
||||
- Don't set `@rendermode` on `<Routes>` in `App.razor` — that makes it global. Per-page mode means individual components opt in.
|
||||
- Don't add `@rendermode` to Identity/Account pages if auth is configured — they must stay static SSR.
|
||||
@@ -0,0 +1,31 @@
|
||||
# {AppName}
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| **Interactivity Mode** | Server |
|
||||
| **Interactivity Scope** | Global |
|
||||
|
||||
## Rendering configuration
|
||||
This project uses global Interactive Server with prerendering.
|
||||
Created with `dotnet new blazor -int Server -ai`.
|
||||
|
||||
All pages are interactive by default via `<Routes @rendermode="InteractiveServer" />` in `App.razor`.
|
||||
|
||||
## Adding new components
|
||||
- Create new `.razor` files in `Components/Pages/` for routable pages or `Components/` for shared components.
|
||||
- All pages are already interactive. No need to add `@rendermode` to individual components.
|
||||
|
||||
## Data access
|
||||
- Components can inject services directly — EF Core DbContext, file system, server-only APIs. No HTTP API layer needed.
|
||||
|
||||
## Environment constraints
|
||||
- Components run on the server via SignalR.
|
||||
- `HttpContext` is NOT available in interactive components — it's only available during the initial static prerender.
|
||||
- Browser APIs are not directly available — use `IJSRuntime` interop.
|
||||
- Every connected user holds a SignalR circuit on the server.
|
||||
|
||||
## Don'ts
|
||||
- Don't add `@rendermode InteractiveServer` to pages — global interactivity is already configured in `App.razor`.
|
||||
- Don't add `@rendermode` to Identity/Account pages if auth is configured — they must stay static SSR.
|
||||
- Don't inject `HttpContext` in interactive components — it's not available during SignalR circuit lifetime.
|
||||
- Don't use browser APIs (localStorage, DOM) directly — use `IJSRuntime` interop instead.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# {AppName}
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| **Interactivity Mode** | Server |
|
||||
| **Interactivity Scope** | Per-page |
|
||||
|
||||
## Rendering configuration
|
||||
This project uses per-page Interactive Server with prerendering.
|
||||
Created with `dotnet new blazor -int Server`.
|
||||
|
||||
Pages are static SSR by default. Only components that explicitly add `@rendermode InteractiveServer` become interactive.
|
||||
|
||||
## Adding new components
|
||||
- Create new `.razor` files in `Components/Pages/` for routable pages or `Components/` for shared components.
|
||||
- New pages are static SSR by default. Only add `@rendermode InteractiveServer` to components that need client-side behavior (live search, real-time updates, complex form interactions).
|
||||
- Static pages can use standard HTML forms with `[SupplyParameterFromForm]` — no interactivity needed.
|
||||
|
||||
## Data access
|
||||
- Components can inject services directly — EF Core DbContext, file system, server-only APIs. No HTTP API layer needed.
|
||||
|
||||
## Environment constraints
|
||||
- Interactive components run on the server via SignalR. `HttpContext` is available in static components but NOT in interactive components during the SignalR circuit lifetime.
|
||||
- Static pages can access `HttpContext` via `[CascadingParameter]`.
|
||||
- Browser APIs are not directly available — use `IJSRuntime` interop in interactive components.
|
||||
|
||||
## Don'ts
|
||||
- Don't add `@rendermode InteractiveServer` to every page — keep read-only content static for performance and lower server memory.
|
||||
- Don't add `@rendermode` to Identity/Account pages if auth is configured — they must stay static SSR.
|
||||
- Don't inject `HttpContext` in interactive components — it's not available during SignalR circuit lifetime.
|
||||
- Don't set `@rendermode` on `<Routes>` in `App.razor` — that makes it global. Per-page mode means individual components opt in.
|
||||
@@ -0,0 +1,33 @@
|
||||
# {AppName}
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| **Interactivity Mode** | None (Static SSR) |
|
||||
| **Interactivity Scope** | N/A |
|
||||
|
||||
## Rendering configuration
|
||||
This project uses static server-side rendering with no interactivity.
|
||||
Created with `dotnet new blazor -int None`.
|
||||
|
||||
Enhanced navigation via `blazor.web.js` is enabled by default, making page transitions feel instant without any interactive runtime.
|
||||
|
||||
## Adding new components
|
||||
- Create new `.razor` files in `Components/Pages/` for routable pages or `Components/` for shared components.
|
||||
- Do NOT add `@rendermode` to any component — this project has no interactive runtime configured.
|
||||
- Forms use standard HTML POST with `[SupplyParameterFromForm]` for model binding.
|
||||
- Query string parameters use `[SupplyParameterFromQuery]`.
|
||||
|
||||
## Data access
|
||||
- Components can inject services directly — EF Core DbContext, file system, server-only APIs. No HTTP API layer needed.
|
||||
|
||||
## Environment constraints
|
||||
- No SignalR circuits, no WebAssembly. All rendering happens on the server.
|
||||
- Forms use HTML POST with `[SupplyParameterFromForm]` and require `<AntiforgeryToken />`.
|
||||
- `HttpContext` is available via `[CascadingParameter]`.
|
||||
- Browser APIs (JS interop) are not available.
|
||||
|
||||
## Don'ts
|
||||
- Don't add `@rendermode InteractiveServer` or any interactive render mode — the project has no interactive runtime registered.
|
||||
- Don't add `AddInteractiveServerComponents()` to `Program.cs` without also updating `App.razor`.
|
||||
- Don't use `@onclick` or other event handlers — they require an interactive render mode. Use form submissions and links for user actions.
|
||||
- Don't use `IJSRuntime` — there is no interactive runtime to execute JavaScript calls.
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
# {AppName}
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| **Interactivity Mode** | WebAssembly |
|
||||
| **Interactivity Scope** | Global |
|
||||
|
||||
## Rendering configuration
|
||||
This project uses global Interactive WebAssembly with prerendering.
|
||||
Created with `dotnet new blazor -int WebAssembly -ai`.
|
||||
|
||||
All pages are interactive by default via `<Routes @rendermode="InteractiveWebAssembly" />` in `App.razor`. Components run entirely in the browser after the initial prerender.
|
||||
|
||||
## Project structure
|
||||
- **{AppName}** (server): Hosts the Blazor app, serves static files, API endpoints.
|
||||
- **{AppName}.Client** (WebAssembly): All interactive UI components. Runs entirely in the browser.
|
||||
|
||||
## Adding new components
|
||||
- All interactive components MUST go in the `.Client` project, not the server project.
|
||||
- New pages go in `{AppName}.Client/Pages/`.
|
||||
- All pages are already interactive. No need to add `@rendermode` to individual components.
|
||||
|
||||
## Data access
|
||||
Interactive components cannot access the database directly. Use this pattern:
|
||||
1. Define an interface in the `.Client` project (e.g., `IDataService`).
|
||||
2. In the `.Client` project, implement it using `HttpClient` to call server APIs.
|
||||
3. In the server project, implement it using direct data access (EF Core DbContext, etc.).
|
||||
4. Register the client implementation in the client `Program.cs` and the server implementation in the server `Program.cs`.
|
||||
5. Expose server data through minimal API endpoints (e.g., `app.MapGet(...)`) that the client implementation calls.
|
||||
6. If the page requires authorization, apply the same auth policy to both the Blazor page (`@attribute [Authorize]`) and the minimal API endpoint (`.RequireAuthorization()`).
|
||||
|
||||
## Service registration
|
||||
- Client-side services go in `{AppName}.Client/Program.cs`.
|
||||
- Server-side services go in `{AppName}/Program.cs`.
|
||||
|
||||
## Environment constraints
|
||||
- Components run in the browser via WebAssembly. No `HttpContext`, no server file system.
|
||||
- All data access goes through `HttpClient` calls to server API endpoints.
|
||||
- The .NET runtime (~10 MB) is downloaded to the browser on first visit and cached.
|
||||
|
||||
## Don'ts
|
||||
- Don't put interactive components in the server project — they work during prerender but fail after WebAssembly handoff.
|
||||
- Don't inject `DbContext` or server-only services in `.Client` project components — use HTTP APIs instead.
|
||||
- Don't add `@rendermode InteractiveWebAssembly` to pages — global interactivity is already configured in `App.razor`.
|
||||
- Don't add `@rendermode` to Identity/Account pages if auth is configured — they must stay static SSR.
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# {AppName}
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| **Interactivity Mode** | WebAssembly |
|
||||
| **Interactivity Scope** | Per-page |
|
||||
|
||||
## Rendering configuration
|
||||
This project uses per-page Interactive WebAssembly with prerendering.
|
||||
Created with `dotnet new blazor -int WebAssembly`.
|
||||
|
||||
Pages are static SSR by default. Only components that explicitly add `@rendermode InteractiveWebAssembly` become interactive and run in the browser.
|
||||
|
||||
## Project structure
|
||||
- **{AppName}** (server): Hosts the Blazor app, serves static files, API endpoints. Static SSR pages and layouts live here.
|
||||
- **{AppName}.Client** (WebAssembly): Interactive components that run in the browser.
|
||||
|
||||
## Adding new components
|
||||
- Interactive components MUST go in the `.Client` project, not the server project.
|
||||
- New pages in the server `Components/Pages/` are static SSR by default.
|
||||
- Only add `@rendermode InteractiveWebAssembly` to components that need client-side interactivity.
|
||||
- Static pages can use standard HTML forms with `[SupplyParameterFromForm]` — no interactivity needed.
|
||||
|
||||
## Data access
|
||||
Interactive components cannot access the database directly. Use this pattern:
|
||||
1. Define an interface in the `.Client` project (e.g., `IDataService`).
|
||||
2. In the `.Client` project, implement it using `HttpClient` to call server APIs.
|
||||
3. In the server project, implement it using direct data access (EF Core DbContext, etc.).
|
||||
4. Register the client implementation in the client `Program.cs` and the server implementation in the server `Program.cs`.
|
||||
5. Expose server data through minimal API endpoints (e.g., `app.MapGet(...)`) that the client implementation calls.
|
||||
6. If the page requires authorization, apply the same auth policy to both the Blazor page (`@attribute [Authorize]`) and the minimal API endpoint (`.RequireAuthorization()`).
|
||||
|
||||
## Service registration
|
||||
- Client-side services go in `{AppName}.Client/Program.cs`.
|
||||
- Server-side services go in `{AppName}/Program.cs`.
|
||||
|
||||
## Environment constraints
|
||||
- Interactive components run in the browser via WebAssembly. No `HttpContext`, no server file system.
|
||||
- Static SSR pages in the server project have full server access.
|
||||
- The .NET runtime (~10 MB) is downloaded to the browser on first visit and cached.
|
||||
|
||||
## Don'ts
|
||||
- Don't put interactive components in the server project — they work during prerender but fail after WebAssembly handoff.
|
||||
- Don't inject `DbContext` or server-only services in `.Client` project components — use HTTP APIs instead.
|
||||
- Don't set `@rendermode` on `<Routes>` in `App.razor` — that makes it global. Per-page mode means individual components opt in.
|
||||
- Don't add `@rendermode` to Identity/Account pages if auth is configured — they must stay static SSR.
|
||||
@@ -0,0 +1,305 @@
|
||||
---
|
||||
license: MIT
|
||||
name: fetch-and-send-data
|
||||
description: Call APIs, load data into components, and handle the async lifecycle in Blazor. USE FOR fetching data from a backend, submitting data to an API, displaying loading/error states, registering HttpClient, building service abstractions for Auto/WebAssembly render modes. DO NOT USE for form validation (see collect-user-input), prerendering persistence (see support-prerendering), or project scaffolding (see create-blazor-project).
|
||||
---
|
||||
|
||||
# Fetch and Send Data
|
||||
|
||||
## Step 1 — Read AGENTS.md
|
||||
|
||||
Check **Interactivity Mode** and **Scope**:
|
||||
|
||||
| Mode | Data access |
|
||||
|------|-------------|
|
||||
| None (Static SSR) | Server-side: inject services/`DbContext`. Use `[StreamRendering]` for loading UX. |
|
||||
| Server | Server-side: inject services/`DbContext`. Guard prerender with `??=` + `[PersistentState]`. |
|
||||
| WebAssembly | Browser-side: `HttpClient` only. No direct server access. |
|
||||
| Auto | Both server and browser. Always go through an API. |
|
||||
|
||||
## Step 2 — Register 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`.
|
||||
|
||||
## Step 3 — Fetch Data
|
||||
|
||||
### 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.
|
||||
|
||||
### Static SSR — StreamRendering
|
||||
|
||||
Without `[StreamRendering]`, the user sees nothing until `OnInitializedAsync` completes:
|
||||
|
||||
```razor
|
||||
@attribute [StreamRendering]
|
||||
```
|
||||
|
||||
Only affects Static SSR. No effect on interactive components.
|
||||
|
||||
### 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.
|
||||
|
||||
## Step 4 — Handle Errors
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.";
|
||||
}
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
## Step 5 — Parameter-Driven Reloading
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
## Step 6 — Send Data
|
||||
|
||||
```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.
|
||||
|
||||
## Step 7 — Service Abstraction for Auto or WebAssembly with Prerendering
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
license: MIT
|
||||
name: plan-ui-change
|
||||
description: >
|
||||
Plan complex Blazor UI features by decomposing them into focused components.
|
||||
USE FOR: building a complex Blazor page with multiple sections, planning
|
||||
component decomposition, designing a multi-section dashboard or layout,
|
||||
breaking down a large UI feature into composable components, pages with
|
||||
sidebars and content panels, any page with 3+ distinct visual sections
|
||||
or multiple interacting sub-features, identifying parent-child relationships
|
||||
and data flow.
|
||||
DO NOT USE FOR: creating new Blazor projects or apps from scratch
|
||||
(use create-blazor-project), implementing a single individual component
|
||||
(use author-component), writing component code with parameters and
|
||||
EventCallback (use author-component), or simple single-component pages.
|
||||
---
|
||||
|
||||
# Plan a Blazor UI Change
|
||||
|
||||
When asked to build a complex UI feature, **plan the component decomposition first, then immediately implement it**. A single monolithic page component is almost never the right answer — break the UI into focused, composable components.
|
||||
|
||||
## Planning Workflow
|
||||
|
||||
### Step 1 — Map the Visual Regions
|
||||
|
||||
Read the request and identify every distinct visual region. Each region that has its own data, behavior, or layout responsibility is a candidate component.
|
||||
|
||||
Draw the component tree:
|
||||
|
||||
```
|
||||
InventoryDashboard (page — owns data, orchestrates layout)
|
||||
├── StockSummaryBar (read-only stats: total items, low-stock count, value)
|
||||
├── InventoryFilters (search box, category dropdown, stock-level toggle)
|
||||
├── InventoryTable (sortable table of products)
|
||||
│ └── InventoryRow (single product row with inline edit/delete)
|
||||
└── AddProductForm (slide-out form for new products)
|
||||
```
|
||||
|
||||
Rules for identifying components:
|
||||
- **Distinct responsibility** — a region owns its own state or behavior → separate component
|
||||
- **Repeated structure** — items in a list, cards in a grid → extract the item template
|
||||
- **Independent interactivity** — a section that handles user input separately from its siblings → separate component
|
||||
- **Size** — any section that would exceed ~150 lines of markup on its own → split it
|
||||
|
||||
### Step 2 — Classify Each Component
|
||||
|
||||
For every component in the tree, determine:
|
||||
|
||||
| Component | Action | Render Mode | State Owned | Lines (est.) |
|
||||
|-----------|--------|-------------|-------------|-------------|
|
||||
| InventoryDashboard | Create | InteractiveServer | product list, filter state | ~80 |
|
||||
| StockSummaryBar | Create | (inherits) | none — receives data | ~30 |
|
||||
| InventoryFilters | Create | (inherits) | search text, selected category | ~60 |
|
||||
| InventoryTable | Create | (inherits) | sort column, sort direction | ~50 |
|
||||
| InventoryRow | Create | (inherits) | inline-edit mode flag | ~60 |
|
||||
| AddProductForm | Create | (inherits) | form model | ~80 |
|
||||
|
||||
**A page component that exceeds ~200 lines of combined markup + code is too large.** If your estimate puts a single component above that, split further.
|
||||
|
||||
### Step 3 — Design Data Flow
|
||||
|
||||
Identify the **state owner** for each piece of data, then map how it flows:
|
||||
|
||||
```
|
||||
InventoryDashboard (owns: products[], filters)
|
||||
│
|
||||
├─ [Parameter] products ──→ StockSummaryBar (reads aggregate stats)
|
||||
│
|
||||
├─ [Parameter] filters ──→ InventoryFilters
|
||||
│ └─ EventCallback<Filters> OnFiltersChanged ──→ InventoryDashboard
|
||||
│
|
||||
├─ [Parameter] filteredProducts ──→ InventoryTable
|
||||
│ └─ [Parameter] product ──→ InventoryRow
|
||||
│ ├─ EventCallback<Product> OnSave ──→ InventoryTable ──→ InventoryDashboard
|
||||
│ └─ EventCallback<Product> OnDelete ──→ InventoryTable ──→ InventoryDashboard
|
||||
│
|
||||
└─ EventCallback<Product> OnProductAdded ←── AddProductForm
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Data always flows **down** through `[Parameter]`
|
||||
- Events always flow **up** through `EventCallback<T>`
|
||||
- The page/parent **owns the data** and passes filtered/transformed views to children
|
||||
- Children **never mutate parameters** — they notify the parent via callbacks
|
||||
- If data must cross more than 2 levels without intermediate components needing it, use a cascading value or a scoped service
|
||||
|
||||
### Step 4 — Identify Reuse Opportunities
|
||||
|
||||
Before creating a new component, check if an existing component in the project can serve the purpose. Look for:
|
||||
- Existing list-item components that match the structure
|
||||
- Shared filter/search components already in the project
|
||||
- Generic components (e.g., `DataTable<T>`, `Pagination`) that accept templates
|
||||
|
||||
If a component will be used in more than one page, place it in a `Shared/` or `Components/` folder.
|
||||
|
||||
### Step 5 — Order the Implementation
|
||||
|
||||
Build bottom-up — leaf components first, then parents that compose them:
|
||||
|
||||
1. **Models/DTOs** — define the data shapes
|
||||
2. **Services** — data access, business logic (interface + implementation)
|
||||
3. **Leaf components** — components with no children (InventoryRow, StockSummaryBar)
|
||||
4. **Container components** — components that compose leaves (InventoryTable, InventoryFilters)
|
||||
5. **Page component** — wires everything together, registers routes
|
||||
6. **Configuration** — DI registration, render mode setup
|
||||
|
||||
Each component should be independently compilable. Never reference a component that doesn't exist yet.
|
||||
|
||||
## Output Format
|
||||
|
||||
Present the plan briefly, then **immediately proceed to implement** — never stop at just the plan or ask for confirmation before writing code. The plan is a thinking tool, not a deliverable.
|
||||
|
||||
```markdown
|
||||
## Component Plan: [Feature Name]
|
||||
|
||||
### Component Tree
|
||||
[ASCII tree showing parent-child relationships]
|
||||
|
||||
### Component Table
|
||||
| Component | Action | Render Mode | Purpose | Est. Lines |
|
||||
|-----------|--------|-------------|---------|------------|
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
### Data Flow
|
||||
[State owner] → [Parameters down] → [EventCallbacks up]
|
||||
|
||||
### Implementation Order
|
||||
1. [First file to create — why]
|
||||
2. [Second file — why]
|
||||
...
|
||||
```
|
||||
|
||||
After outputting the plan, **immediately begin implementing** the components in the order listed. Do not wait for approval or ask "shall I proceed?" — the plan is a guide for you to follow, not a proposal for the user to approve.
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
| Anti-Pattern | Why It's Wrong | Correct Approach |
|
||||
|-------------|----------------|-----------------|
|
||||
| One page component with 500+ lines | Impossible to test, reuse, or maintain | Decompose into focused components |
|
||||
| Passing 10+ parameters through intermediate components | Parameter drilling obscures intent | Use cascading values or a scoped state service |
|
||||
| Child component fetching its own data from an API | Multiple components making redundant calls | Parent owns data, passes via parameters |
|
||||
| Inline rendering of list items with complex markup | Duplicated logic, no reuse, hard to test | Extract item template into its own component |
|
||||
| Building everything in one file then "refactoring later" | Refactoring rarely happens; the monolith ships | Plan the decomposition upfront |
|
||||
| Generic components for one-off usage | Over-engineering adds complexity | Only extract generics when reuse is proven |
|
||||
|
||||
## Guidelines
|
||||
|
||||
- **Plan briefly, then implement.** Write a concise component table and data flow map, then immediately create the `.razor` files — never stop at just the plan.
|
||||
- **Prefer many small components over one large one.** A component with a single clear purpose is easier to understand, test, and reuse.
|
||||
- **State ownership is the first decision.** Before writing fetch logic, decide which component owns the data.
|
||||
- **Build bottom-up.** Create leaf components first so parent components can reference them immediately.
|
||||
- **Name components after what they render**, not what they do internally: `ProductCard` not `ProductRenderer`, `OrderFilters` not `FilterHandler`.
|
||||
@@ -0,0 +1,216 @@
|
||||
---
|
||||
license: MIT
|
||||
name: support-prerendering
|
||||
description: Make interactive Blazor components work correctly with prerendering. USE FOR fixing duplicate data loads, UI flicker during prerender-to-interactive handoff, null references during prerender, persisting state across prerender, disabling prerendering, excluding pages from interactive routing, or detecting whether a component is currently prerendering. DO NOT USE for choosing which render mode to use (see create-blazor-project) or general component authoring (see author-component).
|
||||
---
|
||||
|
||||
# Support Prerendering
|
||||
|
||||
## How Prerendering Works
|
||||
|
||||
Prerendering is **on by default** for all interactive render modes. The server renders the component as static HTML and ships it to the browser immediately. Then the interactive runtime (Server/WebAssembly) loads and re-renders the component with full interactivity.
|
||||
|
||||
This means:
|
||||
- `OnInitializedAsync` runs **twice** — once during prerender (static), once when the interactive runtime attaches.
|
||||
- `OnAfterRenderAsync` is **NOT** called during prerender — only after the interactive render.
|
||||
- Internal navigation between interactive pages (interactive routing) **skips prerendering** — prerendering only happens on full page loads.
|
||||
|
||||
## Step 1 — Read the Project's AGENTS.md
|
||||
|
||||
Check the project's `AGENTS.md` for the **Interactivity Mode** and **Interactivity Scope**:
|
||||
|
||||
| Mode | Prerendering applies? |
|
||||
|------|----------------------|
|
||||
| None (Static SSR) | No — there's no interactive handoff |
|
||||
| Server | Yes |
|
||||
| WebAssembly | Yes |
|
||||
| Auto | Yes |
|
||||
|
||||
If the mode is `None`, this skill doesn't apply.
|
||||
|
||||
## Persist State Across Prerender → Interactive
|
||||
|
||||
The most common prerendering problem: data loaded in `OnInitializedAsync` during prerender is thrown away and re-fetched when the interactive runtime attaches. This causes flicker and duplicate API/DB calls.
|
||||
|
||||
### Recommended: `[PersistentState]` attribute
|
||||
|
||||
Annotate properties to automatically serialize during prerender and restore on interactive activation:
|
||||
|
||||
```razor
|
||||
@page "/forecasts"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<h1>Weather</h1>
|
||||
|
||||
@if (Forecasts is null)
|
||||
{
|
||||
<p>Loading...</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var f in Forecasts)
|
||||
{
|
||||
<p>@f.Date: @f.TemperatureC°C</p>
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
[PersistentState]
|
||||
public WeatherForecast[]? Forecasts { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Forecasts ??= await ForecastService.GetForecastsAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `??=` pattern is critical — it means "only fetch if the property wasn't already restored from prerender state."
|
||||
|
||||
### Multiple instances of the same component
|
||||
|
||||
When the same component type appears multiple times, use `@key` to disambiguate state:
|
||||
|
||||
```razor
|
||||
@foreach (var item in items)
|
||||
{
|
||||
<ItemCard @key="item.Id" />
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced: `PersistentComponentState` service
|
||||
|
||||
For complex scenarios (dynamic keys, custom serialization), use the imperative API:
|
||||
|
||||
```csharp
|
||||
@inject PersistentComponentState ApplicationState
|
||||
|
||||
@code {
|
||||
private List<Order>? orders;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
ApplicationState.RegisterOnPersisting(PersistOrders);
|
||||
|
||||
if (!ApplicationState.TryTakeFromJson<List<Order>>("orders", out var restored))
|
||||
{
|
||||
orders = await OrderService.GetOrdersAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
orders = restored;
|
||||
}
|
||||
}
|
||||
|
||||
private Task PersistOrders()
|
||||
{
|
||||
ApplicationState.PersistAsJson("orders", orders);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Disable Prerendering
|
||||
|
||||
Disable prerendering when a component depends on browser APIs immediately or when the prerender+interactive double render causes problems you can't solve with `[PersistentState]`.
|
||||
|
||||
### On a component definition
|
||||
|
||||
```razor
|
||||
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
||||
```
|
||||
|
||||
Replace `InteractiveServerRenderMode` with `InteractiveWebAssemblyRenderMode` or `InteractiveAutoRenderMode` as needed.
|
||||
|
||||
### On a component instance
|
||||
|
||||
```razor
|
||||
<MyChart @rendermode="new InteractiveServerRenderMode(prerender: false)" />
|
||||
```
|
||||
|
||||
### On the entire app
|
||||
|
||||
In `App.razor`:
|
||||
|
||||
```razor
|
||||
<HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />
|
||||
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
|
||||
```
|
||||
|
||||
Note: A parent's prerendering setting overrides children. If `<Routes>` disables prerendering, individual pages cannot re-enable it.
|
||||
|
||||
## Exclude Pages from Interactive Routing
|
||||
|
||||
In a globally interactive app, some pages may need `HttpContext` (cookies, request headers, response status codes). These pages must render via static SSR, not inside the interactive runtime.
|
||||
|
||||
Use `[ExcludeFromInteractiveRouting]`:
|
||||
|
||||
```razor
|
||||
@page "/privacy"
|
||||
@attribute [ExcludeFromInteractiveRouting]
|
||||
|
||||
<h1>Privacy Policy</h1>
|
||||
```
|
||||
|
||||
This forces a **full page reload** when navigating to this page, exiting interactive routing. The page renders as static SSR with full `HttpContext` access.
|
||||
|
||||
In `App.razor`, conditionally apply the render mode:
|
||||
|
||||
```razor
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<HeadOutlet @rendermode="RenderModeForPage" />
|
||||
</head>
|
||||
<body>
|
||||
<Routes @rendermode="RenderModeForPage" />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
public HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
private IComponentRenderMode? RenderModeForPage =>
|
||||
HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
|
||||
}
|
||||
```
|
||||
|
||||
Replace `InteractiveServer` with the app's configured render mode.
|
||||
|
||||
## Detect Prerender vs Interactive at Runtime
|
||||
|
||||
Use `RendererInfo` to guard code that should only run interactively:
|
||||
|
||||
```csharp
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (RendererInfo.IsInteractive)
|
||||
{
|
||||
// Only runs during the interactive render, not during prerender
|
||||
await StartSignalRConnection();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`RendererInfo` properties:
|
||||
- `IsInteractive` — `false` during prerender, `true` after interactive runtime attaches
|
||||
- `Name` — `"Static"` during prerender, `"Server"` or `"WebAssembly"` when interactive
|
||||
|
||||
## Client Services Fail During Prerender
|
||||
|
||||
Components in the `.Client` project prerender on the server. Services registered only in the client `Program.cs` (e.g., `IWebAssemblyHostEnvironment`) won't be available during prerender.
|
||||
|
||||
Fix by one of:
|
||||
1. **Register a matching service on the server** — both `Program.cs` files provide the service
|
||||
2. **Make the service optional** — use constructor injection with a nullable default: `public MyComponent(IMyService? svc = null)`
|
||||
3. **Create a service abstraction** — interface in `.Client`, implementations in both projects
|
||||
4. **Disable prerendering** for that component
|
||||
|
||||
## Don'ts
|
||||
|
||||
- Don't call JS interop in `OnInitializedAsync` — JS isn't available during prerender. Use `OnAfterRenderAsync(firstRender)`.
|
||||
- Don't assume `OnInitializedAsync` runs once — it runs twice with prerendering. Always use `[PersistentState]` or `??=` guards.
|
||||
- Don't use `HttpContext` in interactive components — it's only available during the static prerender, not during the interactive lifetime. Use `[ExcludeFromInteractiveRouting]` for pages that need it.
|
||||
- Don't disable prerendering as a first resort — it hurts perceived load time and SEO. Use `[PersistentState]` to preserve state instead.
|
||||
@@ -0,0 +1,319 @@
|
||||
---
|
||||
license: MIT
|
||||
name: use-js-interop
|
||||
description: >
|
||||
Add, review, or fix JavaScript interop in Blazor components.
|
||||
USE FOR: calling JavaScript from Blazor, calling .NET from JavaScript,
|
||||
collocated .razor.js modules, IJSRuntime, IJSObjectReference lifecycle,
|
||||
DotNetObjectReference, ElementReference, timing rules for when JS is available,
|
||||
IAsyncDisposable disposal of JS references, server-side JS interop safety.
|
||||
DO NOT USE FOR: general Blazor component authoring without JS interop needs
|
||||
(use author-component), forms (use collect-user-input).
|
||||
---
|
||||
|
||||
# JS Interop in Blazor
|
||||
|
||||
## 1. Collocated JS Modules
|
||||
|
||||
Always use collocated `.razor.js` files with `export` — never global `window.*` functions or `<script>` tags.
|
||||
|
||||
```javascript
|
||||
// ChartPanel.razor.js — placed next to ChartPanel.razor
|
||||
export function initialize(canvas, dotNetRef) { /* ... */ }
|
||||
export function updateData(points) { /* ... */ }
|
||||
export function dispose() { /* ... */ }
|
||||
```
|
||||
|
||||
Import paths: same project = `"./Components/ChartPanel.razor.js"`, RCL = `"./_content/{AssemblyName}/..."`.
|
||||
|
||||
## 2. Lifecycle Timing
|
||||
|
||||
**All JS interop must happen in `OnAfterRenderAsync` or event handlers** — never in `OnInitialized`, `OnParametersSet`, or constructors. JS is not available during server prerendering.
|
||||
|
||||
Use a typed interop wrapper (see Section 4) — never call `InvokeAsync`/`InvokeVoidAsync` with raw string literals:
|
||||
|
||||
```csharp
|
||||
private ChartInterop? _chart;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
_chart = new ChartInterop(JS);
|
||||
await _chart.InitializeAsync(_canvasRef);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Parameter changes**: set a flag in `OnParametersSet`, apply in `OnAfterRenderAsync`:
|
||||
|
||||
```csharp
|
||||
private bool _dataChanged;
|
||||
|
||||
protected override void OnParametersSet() => _dataChanged = true;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender) { /* init */ }
|
||||
else if (_dataChanged && _chart is not null)
|
||||
{
|
||||
_dataChanged = false;
|
||||
await _chart.UpdateDataAsync(DataPoints);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Batch Related Operations
|
||||
|
||||
Each JS interop call crosses the .NET-to-JS boundary (and in Blazor Server, the SignalR circuit). Batching applies in **both directions** — .NET→JS and JS→.NET.
|
||||
|
||||
### .NET → JS: merge consecutive calls
|
||||
|
||||
If the C# side makes two or more JS calls in a row, combine them into one JS function:
|
||||
|
||||
```csharp
|
||||
// ❌ Two round-trips — theme and locale are always applied together
|
||||
await _module.InvokeVoidAsync("applyTheme", theme);
|
||||
await _module.InvokeVoidAsync("applyLocale", locale);
|
||||
|
||||
// ❌ Result of one call feeds into another — both can stay in JS
|
||||
var token = await _module.InvokeAsync<string>("createAccessToken");
|
||||
await _module.InvokeVoidAsync("storeToken", token);
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ✅ One call applies both — no data dependency, no reason for two trips
|
||||
export function applyPreferences(theme, locale) {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
document.documentElement.lang = locale;
|
||||
}
|
||||
|
||||
// ✅ Chain stays in JS — the token never needs to cross the boundary
|
||||
export function createAndStoreToken() {
|
||||
const token = crypto.randomUUID();
|
||||
sessionStorage.setItem('access-token', token);
|
||||
return token;
|
||||
}
|
||||
```
|
||||
|
||||
### JS → .NET: batch callbacks
|
||||
|
||||
When JS needs to send multiple pieces of data back to .NET, send them in a single `invokeMethodAsync` call rather than making separate callbacks:
|
||||
|
||||
```javascript
|
||||
// ❌ Two .NET round-trips from JS
|
||||
await dotNetRef.invokeMethodAsync(ON_VOLUME_CHANGED, volume);
|
||||
await dotNetRef.invokeMethodAsync(ON_PLAYBACK_CHANGED, isPlaying);
|
||||
|
||||
// ✅ One callback with all data
|
||||
await dotNetRef.invokeMethodAsync(ON_PLAYER_STATE_CHANGED, { volume, isPlaying });
|
||||
```
|
||||
|
||||
**Rule**: if two interop calls always happen together from either side, merge them into one function.
|
||||
|
||||
## 4. Typed Interop Wrapper
|
||||
|
||||
Encapsulate interop for a feature in a plain class that owns the module lifecycle:
|
||||
|
||||
```csharp
|
||||
public sealed class ChartInterop : IAsyncDisposable
|
||||
{
|
||||
internal const string ModulePath = "./Components/ChartPanel.razor.js";
|
||||
internal const string InitMethod = "initialize";
|
||||
internal const string UpdateMethod = "updateData";
|
||||
internal const string DisposeMethod = "dispose";
|
||||
|
||||
private readonly IJSRuntime _js;
|
||||
private IJSObjectReference? _module;
|
||||
|
||||
public ChartInterop(IJSRuntime js) => _js = js;
|
||||
|
||||
private async ValueTask<IJSObjectReference> GetModuleAsync()
|
||||
=> _module ??= await _js.InvokeAsync<IJSObjectReference>("import", ModulePath);
|
||||
|
||||
public async ValueTask InitializeAsync(ElementReference canvas)
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
await module.InvokeVoidAsync(InitMethod, canvas);
|
||||
}
|
||||
|
||||
public async ValueTask UpdateDataAsync(IReadOnlyList<DataPoint> points)
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
await module.InvokeVoidAsync(UpdateMethod, points);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_module is not null)
|
||||
{
|
||||
await _module.InvokeVoidAsync(DisposeMethod);
|
||||
await _module.DisposeAsync();
|
||||
}
|
||||
}
|
||||
catch (JSDisconnectedException) { }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The component creates and uses the wrapper with no magic strings:
|
||||
|
||||
```razor
|
||||
@inject IJSRuntime JS
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<canvas @ref="_canvasRef" width="600" height="400"></canvas>
|
||||
|
||||
@code {
|
||||
private ElementReference _canvasRef;
|
||||
private ChartInterop? _chart;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
_chart = new ChartInterop(JS);
|
||||
await _chart.InitializeAsync(_canvasRef);
|
||||
}
|
||||
}
|
||||
|
||||
async ValueTask IAsyncDisposable.DisposeAsync()
|
||||
{
|
||||
if (_chart is not null)
|
||||
await _chart.DisposeAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Prefer a concrete class over interface + implementation for interop wrappers. For unit testing, substitute `IJSRuntime` directly (it is already an interface).
|
||||
|
||||
## 5. DotNetObjectReference for JS-to-.NET Callbacks
|
||||
|
||||
```csharp
|
||||
_dotNetRef = DotNetObjectReference.Create(this);
|
||||
await _module.InvokeVoidAsync("initialize", _dotNetRef);
|
||||
```
|
||||
|
||||
On the JS side, wrap the `dotNetRef` in a class. Use `async`/`await` with `try/catch` (not `.catch()`) to guard against circuit loss. Define .NET method name constants at the top:
|
||||
|
||||
```javascript
|
||||
const ON_CLIPBOARD_CHANGED = 'OnClipboardChanged';
|
||||
|
||||
class ClipboardMonitor {
|
||||
#dotNetRef;
|
||||
#abortController;
|
||||
|
||||
constructor(dotNetRef) {
|
||||
this.#dotNetRef = dotNetRef;
|
||||
this.#abortController = new AbortController();
|
||||
}
|
||||
|
||||
start() {
|
||||
document.addEventListener('copy', async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
await this.#dotNetRef.invokeMethodAsync(ON_CLIPBOARD_CHANGED, text);
|
||||
} catch { /* circuit disconnected or clipboard denied */ }
|
||||
}, { signal: this.#abortController.signal });
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.#abortController.abort();
|
||||
}
|
||||
}
|
||||
|
||||
let monitor;
|
||||
export function initialize(dotNetRef) {
|
||||
monitor = new ClipboardMonitor(dotNetRef);
|
||||
monitor.start();
|
||||
}
|
||||
|
||||
export function dispose() {
|
||||
monitor?.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
- `[JSInvokable]` methods **must be `public`** — private/internal silently fails at runtime
|
||||
- Wrap `StateHasChanged` in `InvokeAsync` inside `[JSInvokable]` callbacks:
|
||||
```csharp
|
||||
[JSInvokable]
|
||||
public async Task OnClipboardChanged(string text)
|
||||
{
|
||||
await InvokeAsync(() => { _lastClipboard = text; StateHasChanged(); });
|
||||
}
|
||||
```
|
||||
- Always `try/catch` around `invokeMethodAsync` in JS — circuit loss throws
|
||||
- Use `const` for .NET method name strings in JS — prevents typo bugs that silently fail
|
||||
- Dispose `DotNetObjectReference` in `DisposeAsync`
|
||||
|
||||
## 6. Disposal and Server Safety
|
||||
|
||||
Always implement `IAsyncDisposable`. Call JS cleanup first, then dispose references. Catch `JSDisconnectedException` for Blazor Server circuit loss:
|
||||
|
||||
```csharp
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_module is not null)
|
||||
{
|
||||
await _module.InvokeVoidAsync("dispose");
|
||||
await _module.DisposeAsync();
|
||||
}
|
||||
}
|
||||
catch (JSDisconnectedException) { }
|
||||
|
||||
_dotNetRef?.Dispose();
|
||||
}
|
||||
```
|
||||
|
||||
Never use sync `IDisposable` for JS interop cleanup — `InvokeVoidAsync` returns `ValueTask` and must be awaited.
|
||||
|
||||
## 7. ElementReference
|
||||
|
||||
Pass DOM elements via `@ref`, not string IDs:
|
||||
|
||||
```razor
|
||||
<canvas @ref="_canvasRef" width="600" height="400"></canvas>
|
||||
```
|
||||
|
||||
```csharp
|
||||
await _chart.InitializeAsync(_canvasRef);
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] JS is in collocated `.razor.js` with `export` — no `window.*` globals
|
||||
- [ ] All interop in `OnAfterRenderAsync` or event handlers — never during prerender
|
||||
- [ ] `IAsyncDisposable` catches `JSDisconnectedException`
|
||||
- [ ] `DotNetObjectReference` disposed in `DisposeAsync`; JS side has `try/catch` around `invokeMethodAsync`
|
||||
- [ ] `[JSInvokable]` methods are `public` and use `await InvokeAsync(StateHasChanged)`
|
||||
- [ ] `InvokeVoidAsync` used when no return value is needed
|
||||
- [ ] `ElementReference` instead of string IDs
|
||||
- [ ] Related operations batched into single interop calls (both .NET→JS and JS→.NET)
|
||||
|
||||
## Common Mistakes Checklist
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| Using JS for something achievable with CSS | Use CSS custom properties, `data-` attributes, pseudo-classes |
|
||||
| Many fine-grained interop calls | Batch into coarse functions — both .NET→JS and JS→.NET |
|
||||
| Component imports JS module directly | Encapsulate in a strongly typed interop class |
|
||||
| Magic strings for method names / module paths | Define `internal const` fields in the interop class |
|
||||
| Interface + implementation for interop wrapper | Use a plain class; mock `IJSRuntime` for tests instead |
|
||||
| JS calls in `OnInitializedAsync` | Move to `OnAfterRenderAsync(firstRender)` |
|
||||
| `InvokeAsync<object>` for void calls | Use `InvokeVoidAsync` |
|
||||
| `IDisposable` with fire-and-forget JS | Use `IAsyncDisposable` with `await` |
|
||||
| Global `window.*` JS functions | Use collocated `.razor.js` with `export` |
|
||||
| String element IDs passed to JS | Use `ElementReference` with `@ref` |
|
||||
| `[JSInvokable]` on private method | Must be `public` — silently fails otherwise |
|
||||
| `DotNetObjectReference` not disposed | Dispose in `DisposeAsync` — causes memory leak |
|
||||
| `StateHasChanged()` without `InvokeAsync` | Wrap in `await InvokeAsync(() => { StateHasChanged(); })` |
|
||||
| JS `invokeMethodAsync` without error handling | Wrap in `try/catch` — circuit loss throws |
|
||||
| Bare `dotNetRef` in JS event handlers | Wrap in a class with `#dotNetRef` private field |
|
||||
| Magic strings in JS `invokeMethodAsync` calls | Use `const` at module top — typos silently fail at runtime |
|
||||
| JS calls in `OnParametersSetAsync` | Track changes, apply in `OnAfterRenderAsync` with guard |
|
||||
| No null check before calling module | Check `module is not null` before use |
|
||||
@@ -0,0 +1,114 @@
|
||||
scenarios:
|
||||
- name: "Blazor Server app with CascadingAuthenticationState"
|
||||
prompt: |
|
||||
Convert this .NET 7 Blazor Server app to a .NET 8 Blazor Web App. The app uses
|
||||
authentication with CascadingAuthenticationState. Provide the complete updated files.
|
||||
The app should no longer use AddServerSideBlazor or MapBlazorHub.
|
||||
|
||||
Program.cs:
|
||||
```csharp
|
||||
using BlazorAuthApp;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddRazorPages();
|
||||
builder.Services.AddServerSideBlazor();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
app.MapBlazorHub();
|
||||
app.MapFallbackToPage("/_Host");
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
App.razor:
|
||||
```razor
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
<RedirectToLogin />
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
</CascadingAuthenticationState>
|
||||
```
|
||||
|
||||
Pages/_Host.cshtml:
|
||||
```html
|
||||
@page "/"
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@namespace BlazorAuthApp.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="~/" />
|
||||
<link href="BlazorAuthApp.styles.css" rel="stylesheet" />
|
||||
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
|
||||
</head>
|
||||
<body>
|
||||
<component type="typeof(App)" render-mode="ServerPrerendered" />
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
<environment include="Staging,Production">
|
||||
An error has occurred. This application may no longer respond until reloaded.
|
||||
</environment>
|
||||
<environment include="Development">
|
||||
An unhandled exception has occurred. See browser dev tools for details.
|
||||
</environment>
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
assertions:
|
||||
- type: output_contains
|
||||
value: "AddCascadingAuthenticationState"
|
||||
- type: output_contains
|
||||
value: "AddRazorComponents"
|
||||
- type: output_contains
|
||||
value: "MapRazorComponents"
|
||||
- type: output_contains
|
||||
value: "UseAntiforgery"
|
||||
- type: output_not_matches
|
||||
pattern: "AddServerSideBlazor\\s*\\("
|
||||
- type: output_not_matches
|
||||
pattern: "MapBlazorHub\\s*\\("
|
||||
- type: exit_success
|
||||
rubric:
|
||||
- "Removes the CascadingAuthenticationState component wrapper from App.razor/Routes.razor"
|
||||
- "Adds builder.Services.AddCascadingAuthenticationState() to Program.cs"
|
||||
- "Places UseAntiforgery after UseAuthentication and UseAuthorization in the middleware pipeline"
|
||||
- "Keeps AuthorizeRouteView in the Routes component"
|
||||
- "Does NOT switch to static rendering — preserves interactive server rendering"
|
||||
timeout: 120
|
||||
@@ -0,0 +1,221 @@
|
||||
scenarios:
|
||||
- name: "Author a data-loading search component"
|
||||
prompt: |
|
||||
Write a Blazor component called ProductSearch that will be used on a catalog browsing page. Here are the service contracts the application already registers in DI:
|
||||
|
||||
```csharp
|
||||
public record Product(int Id, string Name, decimal Price, string ImageUrl);
|
||||
|
||||
public interface IProductService
|
||||
{
|
||||
/// <summary>
|
||||
/// Searches products by query string. Throws on network failure.
|
||||
/// Respects the cancellation token — callers should cancel when results are no longer needed.
|
||||
/// </summary>
|
||||
Task<List<Product>> SearchAsync(string query, CancellationToken ct);
|
||||
}
|
||||
```
|
||||
|
||||
The parent page will use the component like this:
|
||||
|
||||
```razor
|
||||
<ProductSearch OnProductSelected="HandleSelection" />
|
||||
```
|
||||
|
||||
The component should:
|
||||
- Have a search text input. When the user stops typing for 300ms, search automatically
|
||||
- Call IProductService.SearchAsync to fetch results
|
||||
- Display each product as a card showing Name, Price, and ImageUrl
|
||||
- Notify the parent when a card is clicked so it can navigate to the product detail page
|
||||
- Handle all the states the component can be in (fetching, nothing found, failure, etc.)
|
||||
- If the user types again while a search is in flight, the old search should be abandoned
|
||||
- Clean up properly when the component is removed from the page
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "IAsyncDisposable"
|
||||
- type: "output_contains"
|
||||
value: "CancellationToken"
|
||||
- type: "output_contains"
|
||||
value: "EventCallback"
|
||||
- type: "output_matches"
|
||||
pattern: "(@if|@else)"
|
||||
rubric:
|
||||
- "Handles all visual states: loading, empty, error, and loaded — no missing branches"
|
||||
- "Uses EventCallback<Product> for product selection notification to the parent"
|
||||
- "Cancels the previous CancellationTokenSource when a new search starts"
|
||||
- "Implements IAsyncDisposable and cancels/disposes resources in DisposeAsync"
|
||||
- "Uses a private field for mutable state — does not mutate [Parameter] properties"
|
||||
- "Debounces search input with Task.Delay + CancellationTokenSource"
|
||||
timeout: 180
|
||||
|
||||
- name: "Author a multi-step wizard with async validation and shared state"
|
||||
prompt: |
|
||||
Author a set of Blazor components for a checkout wizard with async validation.
|
||||
Here are the models and service:
|
||||
|
||||
```csharp
|
||||
public class ShippingInfo
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string Address { get; set; } = "";
|
||||
public string City { get; set; } = "";
|
||||
public string ZipCode { get; set; } = "";
|
||||
}
|
||||
|
||||
public class PaymentInfo
|
||||
{
|
||||
public string CardNumber { get; set; } = "";
|
||||
public string ExpiryDate { get; set; } = "";
|
||||
}
|
||||
|
||||
public class Order
|
||||
{
|
||||
public ShippingInfo Shipping { get; set; } = new();
|
||||
public PaymentInfo Payment { get; set; } = new();
|
||||
}
|
||||
|
||||
public interface IAddressValidator
|
||||
{
|
||||
Task<bool> ValidateAddressAsync(string address, string city, string zip, CancellationToken ct);
|
||||
}
|
||||
```
|
||||
|
||||
Write the .razor component code for:
|
||||
1. CheckoutWizard.razor — manages which step is active and holds the order state.
|
||||
When "Place Order" is clicked, the parent of CheckoutWizard receives the completed order.
|
||||
2. ShippingStep.razor — collects shipping address fields. When "Next" is clicked,
|
||||
validates the address asynchronously using IAddressValidator. While validating,
|
||||
show a loading indicator and disable the button. If the user goes back and
|
||||
changes fields then clicks Next again, cancel the previous validation.
|
||||
3. PaymentStep.razor — collects payment info, has "Back" and "Place Order" buttons.
|
||||
|
||||
The wizard must handle disposal correctly — if the user navigates away mid-validation,
|
||||
the pending async operation should be cancelled cleanly.
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "EventCallback"
|
||||
- type: "output_contains"
|
||||
value: "[Parameter]"
|
||||
- type: "output_contains"
|
||||
value: "CancellationToken"
|
||||
- type: "output_matches"
|
||||
pattern: "(CheckoutWizard|ShippingStep|PaymentStep)"
|
||||
rubric:
|
||||
- "Parent (CheckoutWizard) owns the order state and passes data to children via [Parameter]"
|
||||
- "Children communicate back to the parent through EventCallback"
|
||||
- "Steps are separate component files, not inline blocks in the parent"
|
||||
- "ShippingStep cancels the previous validation when a new one starts — uses CancellationTokenSource that is replaced on each attempt"
|
||||
- "Implements IAsyncDisposable and cancels the CTS in DisposeAsync — pending validations are cancelled when navigating away"
|
||||
- "Parameters are public auto-properties with { get; set; }"
|
||||
timeout: 120
|
||||
|
||||
- name: "Author a generic data table component"
|
||||
prompt: |
|
||||
Write a generic Blazor component called DataTable that works with any item type.
|
||||
The parent should be able to customize how headers and rows are rendered by passing
|
||||
in their own templates. Here's an example of how a consumer would use it:
|
||||
|
||||
```razor
|
||||
<DataTable Items="@people" OnRowClick="HandleRowClick">
|
||||
<HeaderTemplate>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
</HeaderTemplate>
|
||||
<RowTemplate>
|
||||
<td>@context.Name</td>
|
||||
<td>@context.Email</td>
|
||||
</RowTemplate>
|
||||
</DataTable>
|
||||
```
|
||||
|
||||
When there are no items, it should show a sensible default empty state, but the
|
||||
consumer should be able to override that too.
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "@typeparam"
|
||||
- type: "output_matches"
|
||||
pattern: "RenderFragment"
|
||||
- type: "output_contains"
|
||||
value: "EventCallback"
|
||||
rubric:
|
||||
- "Uses @typeparam to make the component generic"
|
||||
- "RowTemplate is RenderFragment<TItem> — not RenderFragment or a delegate"
|
||||
- "HeaderTemplate and EmptyTemplate are RenderFragment (non-generic)"
|
||||
- "Uses @key on repeated row elements for efficient diffing in loops"
|
||||
- "Handles the empty state with a default message and an optional override template"
|
||||
- "Uses EventCallback<TItem> for OnRowClick"
|
||||
- "Items parameter is IReadOnlyList<TItem> or List<TItem>"
|
||||
timeout: 120
|
||||
|
||||
- name: "Author a real-time notification badge component"
|
||||
prompt: |
|
||||
Write a Blazor component called NotificationBadge. Here are the service contracts:
|
||||
|
||||
```csharp
|
||||
public record Notification(int Id, string Title, DateTime ReceivedAt);
|
||||
|
||||
public interface INotificationService
|
||||
{
|
||||
event Action<Notification> OnNotificationReceived;
|
||||
Task<int> GetUnreadCountAsync();
|
||||
}
|
||||
```
|
||||
|
||||
The component should:
|
||||
- Show a badge with the current unread count
|
||||
- Update the count in real time when OnNotificationReceived fires
|
||||
- Also poll GetUnreadCountAsync every 30 seconds as a fallback
|
||||
- When clicked, the parent needs to know so it can open a notification panel
|
||||
- Clean up everything when the component is removed
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "IAsyncDisposable"
|
||||
- type: "output_contains"
|
||||
value: "DisposeAsync"
|
||||
- type: "output_contains"
|
||||
value: "InvokeAsync"
|
||||
- type: "output_matches"
|
||||
pattern: "(\\+=|OnNotificationReceived)"
|
||||
rubric:
|
||||
- "Implements IAsyncDisposable and unsubscribes from the event in DisposeAsync"
|
||||
- "Subscribes to the event in OnInitialized and unsubscribes in DisposeAsync with -="
|
||||
- "Uses InvokeAsync to marshal the event callback onto the Blazor sync context before calling StateHasChanged"
|
||||
- "Cancels CTS in DisposeAsync to stop polling"
|
||||
- "Event handler properly dispatches exceptions to the renderer"
|
||||
- "Polls using Task.Delay in a loop or PeriodicTimer"
|
||||
timeout: 120
|
||||
|
||||
- name: "Author a sortable list with code-behind pattern"
|
||||
prompt: |
|
||||
Write a Blazor component called SortableList using the code-behind pattern
|
||||
(SortableList.razor + SortableList.razor.cs). It should work with any item type.
|
||||
|
||||
Here's an example of how a consumer would use it:
|
||||
|
||||
```razor
|
||||
<SortableList Items="@tasks" OnOrderChanged="HandleReorder">
|
||||
<ItemTemplate>
|
||||
<span>@context.Title</span>
|
||||
</ItemTemplate>
|
||||
</SortableList>
|
||||
```
|
||||
|
||||
Each item should have "Move Up" and "Move Down" buttons. After each reorder,
|
||||
the parent needs the updated list. The component must not modify the list
|
||||
that was passed in — it should work on its own internal copy.
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "partial class"
|
||||
- type: "output_contains"
|
||||
value: "EventCallback"
|
||||
- type: "output_matches"
|
||||
pattern: "(private|_items|localItems|internalItems)"
|
||||
- type: "output_matches"
|
||||
pattern: "RenderFragment"
|
||||
rubric:
|
||||
- "Uses the code-behind pattern: .razor file for markup, .razor.cs for logic with partial class"
|
||||
- "Does NOT mutate the [Parameter] Items property — copies to a private field"
|
||||
- "Copies Items to a local field in OnParametersSet"
|
||||
- "Uses @key in the loop for efficient diffing"
|
||||
- "Uses EventCallback to notify the parent of reorder"
|
||||
timeout: 120
|
||||
@@ -0,0 +1,102 @@
|
||||
scenarios:
|
||||
- name: "Event registration with custom validation"
|
||||
setup: &blazor_project
|
||||
commands:
|
||||
- "dotnet new blazor --interactivity server --no-https --force"
|
||||
prompt: |
|
||||
I have an existing Blazor Web App with Interactive Server (per-page).
|
||||
|
||||
I need an interactive /register page for event registration:
|
||||
- Attendee name (required)
|
||||
- Email (required, valid email)
|
||||
- Number of guests (0-5)
|
||||
- Preferred session (Morning, Afternoon, Evening)
|
||||
- Special dietary requirements (free text, optional)
|
||||
|
||||
Here's the catch: after the user fills everything in and clicks Register,
|
||||
I need to check with the server whether that email is already registered
|
||||
for this event. If it is, show an error message on the email field saying
|
||||
"This email is already registered" — not a generic error, but a field-specific
|
||||
error that appears right next to the email input. The user should be able to
|
||||
change the email and try again.
|
||||
|
||||
Create a RegistrationService with a stub ValidateAsync method that returns
|
||||
field-specific errors (simulate the email already being registered for a
|
||||
hardcoded test email like "taken@example.com").
|
||||
assertions:
|
||||
- type: "file_exists"
|
||||
path: "**/Register.razor"
|
||||
- type: "file_contains"
|
||||
path: "**/Register.razor"
|
||||
value: "EditForm"
|
||||
- type: "file_contains"
|
||||
path: "**/Register.razor"
|
||||
value: "ValidationMessage"
|
||||
- type: "file_contains"
|
||||
path: "**/Register.razor"
|
||||
value: "DataAnnotationsValidator"
|
||||
rubric:
|
||||
- "Uses a custom validator component (or ValidationMessageStore directly) to display server-returned field-specific errors — the email duplicate error appears next to the email field via ValidationMessage, not as a generic message"
|
||||
- "Model uses data annotation attributes for basic validation (Required, EmailAddress, Range) with DataAnnotationsValidator"
|
||||
- "Submit handler calls the registration service, checks for server-returned errors, and displays them on the correct fields without losing the user's input"
|
||||
- "The form lets the user correct the email and resubmit — errors clear when the field changes"
|
||||
- "Uses InputText, InputNumber, InputSelect or InputRadioGroup for the respective fields"
|
||||
timeout: 600
|
||||
|
||||
|
||||
- name: "Multi-step booking form with cross-field validation"
|
||||
setup: *blazor_project
|
||||
prompt: |
|
||||
I have a Blazor Web App with per-page interactivity (Server mode).
|
||||
|
||||
I need a /book-appointment page for scheduling medical appointments.
|
||||
The form has two steps on the same page (not separate routes):
|
||||
|
||||
STEP 1 (always visible):
|
||||
- Patient name (required)
|
||||
- Email (required, valid format)
|
||||
- Phone (required)
|
||||
- Insurance provider (dropdown: Aetna, BlueCross, Cigna, UnitedHealth, None)
|
||||
|
||||
STEP 2 (appears after step 1 validation passes):
|
||||
- Preferred doctor (dropdown: loaded from an AppointmentService)
|
||||
- Date (required, must be in the future)
|
||||
- Time slot (dropdown: available slots from AppointmentService based on doctor + date)
|
||||
- Reason for visit (required, max 500 chars)
|
||||
|
||||
When the user clicks "Check Availability" between steps, call
|
||||
AppointmentService.ValidatePatientAsync which checks server-side rules:
|
||||
- If the email belongs to a patient with an outstanding balance, return
|
||||
an error on the Email field: "Account has outstanding balance"
|
||||
- If the insurance provider is "None" and the selected doctor doesn't
|
||||
accept self-pay, return an error on the Doctor field: "Selected doctor
|
||||
does not accept self-pay patients"
|
||||
- If the selected time slot was just taken (race condition), return an
|
||||
error on the TimeSlot field: "This slot is no longer available"
|
||||
|
||||
These server errors must appear next to the specific fields — not as
|
||||
a generic error banner. The user should be able to correct the field
|
||||
and try again without losing other form data.
|
||||
|
||||
Create stub AppointmentService that simulates these validations:
|
||||
reject "taken@example.com" for balance, reject "None" insurance
|
||||
with doctor "Dr. Smith", and reject timeslot "10:00 AM" as taken.
|
||||
|
||||
Make this page interactive with @rendermode InteractiveServer.
|
||||
assertions:
|
||||
- type: "file_exists"
|
||||
path: "**/BookAppointment.razor"
|
||||
- type: "file_contains"
|
||||
path: "**/BookAppointment.razor"
|
||||
value: "EditForm"
|
||||
- type: "file_contains"
|
||||
path: "**/BookAppointment.razor"
|
||||
value: "ValidationMessage"
|
||||
rubric:
|
||||
- "Server-returned errors appear next to the specific field via ValidationMessage — uses ValidationMessageStore or equivalent to add field-level errors from the service response, not manual conditional divs"
|
||||
- "After server validation failure, the user can change the flagged field and resubmit — errors clear on the relevant field without losing other form data"
|
||||
- "Uses data annotation attributes for client-side validation (Required, EmailAddress, MaxLength, custom date validator) with DataAnnotationsValidator"
|
||||
- "Multi-step progression — step 2 fields only appear after step 1 validates successfully, providing progressive disclosure"
|
||||
- "Time slots load dynamically based on doctor and date selection — cascading dropdown behavior within the form"
|
||||
timeout: 900
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
scenarios:
|
||||
- name: "Login and account management in a globally interactive app"
|
||||
setup: &server_project
|
||||
commands:
|
||||
- "dotnet new blazor --interactivity server --all-interactive --no-https --force"
|
||||
prompt: |
|
||||
I have a Blazor Web App with Interactive Server mode applied globally
|
||||
(all pages are interactive by default).
|
||||
|
||||
I need to add ASP.NET Core Identity-based authentication. Here's where
|
||||
I'm stuck:
|
||||
|
||||
1. I need a /account/login page with email/password that calls
|
||||
SignInManager.PasswordSignInAsync. But when I try to use SignInManager
|
||||
in my interactive page, I get an error about HttpContext not being
|
||||
available. The whole app is globally interactive — how do I make
|
||||
login work?
|
||||
|
||||
2. Same problem for /account/register using UserManager.CreateAsync —
|
||||
it also needs HttpContext for cookie authentication.
|
||||
|
||||
3. After login, the user should be redirected to / (homepage). After
|
||||
registration, redirect to login. Both pages need proper form
|
||||
validation (required fields, email format).
|
||||
|
||||
4. I also need a /account/logout endpoint that signs out. But wait —
|
||||
signing out also needs HttpContext... same problem again?
|
||||
|
||||
5. Add a "Logged in as {email} | Logout" display in the nav when
|
||||
authenticated, and "Login | Register" links when not.
|
||||
|
||||
6. The rest of the app must stay fully interactive — I don't want to
|
||||
lose global interactivity for my normal pages just because auth
|
||||
pages have special needs.
|
||||
|
||||
Set up Identity services in Program.cs with an in-memory EF store.
|
||||
I don't need user seeding — just the pages and plumbing.
|
||||
assertions:
|
||||
- type: "file_contains"
|
||||
path: "**/*ogin*"
|
||||
value: "ExcludeFromInteractiveRouting"
|
||||
- type: "file_contains"
|
||||
path: "**/App.razor"
|
||||
value: "AcceptsInteractiveRouting"
|
||||
- type: "file_contains"
|
||||
path: "**/*ogin*"
|
||||
value: "SignInManager"
|
||||
rubric:
|
||||
- "Login and register pages use [ExcludeFromInteractiveRouting] to render as static SSR — explains that SignInManager and UserManager need HttpContext which is unavailable in interactive components"
|
||||
- "App.razor conditionally applies the render mode using HttpContext.AcceptsInteractiveRouting() — returns null for excluded pages and InteractiveServer for everything else"
|
||||
- "Identity services are registered in Program.cs (AddIdentity or AddIdentityCore with AddEntityFrameworkStores and AddSignInManager) and authentication middleware is added"
|
||||
- "Logout is handled correctly (either a static page or a minimal API endpoint) — does not attempt HttpContext operations inside an interactive component"
|
||||
- "Auth state is reflected in the UI — shows user info when logged in, login/register links when not"
|
||||
timeout: 1200
|
||||
|
||||
- name: "Multi-tier app with WebAssembly auth"
|
||||
setup:
|
||||
commands:
|
||||
- "dotnet new blazor --interactivity auto --no-https --force"
|
||||
prompt: |
|
||||
I have a Blazor Web App using Auto interactivity mode (per-page). Some
|
||||
pages will run on Server, others on WebAssembly.
|
||||
|
||||
I need to add authentication that works across both render modes:
|
||||
|
||||
1. Create a custom AuthenticationStateProvider on the server that
|
||||
simulates a logged-in user "bob@example.com" with roles "User" and
|
||||
"Editor". No real database needed — just hardcode the claims.
|
||||
|
||||
2. A /profile page (interactive, any mode) that displays the authenticated
|
||||
user's name and all their roles. It should work regardless of whether
|
||||
it's currently running on Server or WebAssembly.
|
||||
|
||||
3. A /editor page (interactive, any mode) restricted to the "Editor" role.
|
||||
Show an "Access denied" message for users without the role.
|
||||
|
||||
4. A /viewer page (interactive, any mode) accessible to any authenticated
|
||||
user.
|
||||
|
||||
The key requirement: after the page initially loads and the WebAssembly
|
||||
runtime takes over, the user's identity and roles must still be available.
|
||||
I've heard that auth state can get lost when the client-side runtime
|
||||
activates — make sure that doesn't happen.
|
||||
assertions:
|
||||
- type: "file_contains"
|
||||
path: "**/Program.cs"
|
||||
value: "AddAuthenticationStateSerialization"
|
||||
- type: "file_contains"
|
||||
path: "**/*.razor"
|
||||
value: '[Authorize'
|
||||
rubric:
|
||||
- "Server Program.cs calls AddAuthenticationStateSerialization so auth state is serialized into the prerendered HTML — without this the WebAssembly runtime starts with an anonymous user"
|
||||
- "Client Program.cs calls AddAuthenticationStateDeserialization so the WASM runtime can reconstitute the auth state from the serialized data"
|
||||
- "Profile page displays user name and roles that remain correct after WebAssembly takes over from prerendering — the auth state is not lost during the handoff"
|
||||
- "Editor page enforces role-based access via [Authorize(Roles = ...)] or AuthorizeView with Roles — unauthorized users see a denied message, not the page content"
|
||||
timeout: 600
|
||||
@@ -0,0 +1,107 @@
|
||||
scenarios:
|
||||
- name: "Warehouse dashboard with site selector and live stock alerts"
|
||||
setup: &server_project
|
||||
commands:
|
||||
- "dotnet new blazor --interactivity server --no-https --force"
|
||||
prompt: |
|
||||
I have a Blazor Web App using per-page interactivity (InteractiveServer).
|
||||
|
||||
I need to coordinate shared state between components that are NOT in a
|
||||
parent-child relationship. The scenario is a warehouse management page
|
||||
where several unrelated components must stay synchronized through a
|
||||
shared service — they can't pass parameters to each other directly.
|
||||
|
||||
1. A "current warehouse" dropdown that lives in the MainLayout. The
|
||||
layout renders statically (no interactivity attribute). The dropdown
|
||||
itself must be interactive so it can handle change events. Every
|
||||
interactive component on every page should automatically know which
|
||||
warehouse is currently selected without manually querying for it.
|
||||
When the user switches warehouses, everything on the current page
|
||||
should immediately update to reflect the new site.
|
||||
|
||||
2. A live low-stock alert count that checks for items below minimum
|
||||
levels every 10 seconds. Create an InventoryAlertService with a
|
||||
GetLowStockCountAsync(string warehouseId) method that simulates a
|
||||
database query (return a random number between 0 and 15). The alert
|
||||
count should appear as a badge in the sidebar navigation AND on the
|
||||
main dashboard page. Polling should start when the dashboard is
|
||||
visible and stop when the user leaves the page. The sidebar badge
|
||||
should also refresh when new data arrives.
|
||||
|
||||
3. When polling detects new data, ALL components subscribed to alerts
|
||||
should update — not just the one that triggered the poll. This means
|
||||
you need a proper notification/event mechanism between components
|
||||
that aren't in a parent-child relationship.
|
||||
|
||||
4. Multiple warehouse managers using the app simultaneously must each
|
||||
see their own independent warehouse selection and alert counts.
|
||||
One manager switching to "Warehouse B" must not affect another
|
||||
manager viewing "Warehouse A".
|
||||
|
||||
Build the shared state service with change notification events, the
|
||||
polling mechanism, a warehouse selector component, an alert badge
|
||||
component, and a page that ties it all together.
|
||||
assertions:
|
||||
- type: "file_contains"
|
||||
path: "**/Program.cs"
|
||||
value: "AddScoped"
|
||||
- type: "file_contains"
|
||||
path: "**/*.razor"
|
||||
value: "InvokeAsync"
|
||||
- type: "file_contains"
|
||||
path: "**/*.razor"
|
||||
value: "IDisposable"
|
||||
rubric:
|
||||
- "The selected warehouse is available to all interactive page components without each one fetching it independently — shared state works despite the static layout boundary"
|
||||
- "When the user picks a different warehouse, all subscribed components re-render with the new value without a full page reload"
|
||||
- "Polling fires on a background timer, so the component marshals UI updates through InvokeAsync(StateHasChanged) — calling StateHasChanged directly from a non-Blazor thread would throw"
|
||||
- "Each user's state is isolated: state services use scoped lifetime (per circuit) — not singleton, which would leak one user's selection to another"
|
||||
- "Polling is stopped and event subscriptions are removed when components are disposed — implements IDisposable or IAsyncDisposable and cleans up all resources"
|
||||
- "Alert updates notify ALL subscribed components (sidebar badge AND dashboard) — multiple subscribers react to the same state change"
|
||||
timeout: 900
|
||||
|
||||
- name: "Multi-tenant notification hub with cross-component fan-out"
|
||||
setup:
|
||||
commands:
|
||||
- "dotnet new blazor --interactivity server --no-https --force"
|
||||
prompt: |
|
||||
I have a Blazor Web App with per-page Interactive Server mode (not global).
|
||||
The MainLayout renders statically (no @rendermode attribute on it).
|
||||
|
||||
I need to build an app-wide theme system with these requirements:
|
||||
|
||||
1. A theme service that holds the current theme (Light/Dark/System) and
|
||||
a user's preferred accent color (string). Any component on any page
|
||||
should be able to read AND change these values.
|
||||
|
||||
2. When ANY component changes the theme (e.g., a toggle in the sidebar
|
||||
on one page), all OTHER components consuming the theme must immediately
|
||||
update — including components in the static layout area (like a theme
|
||||
indicator in the header).
|
||||
|
||||
3. I also want a "user preferences" panel on a /settings page where the
|
||||
user can change both theme and accent color. Saving here must update
|
||||
the header theme indicator AND any themed components on any page.
|
||||
|
||||
4. Each user (circuit) must have independent theme state — one user
|
||||
switching to dark mode must not affect another user.
|
||||
|
||||
5. When navigating between pages (enhanced nav), the theme choice must
|
||||
persist without page reload.
|
||||
|
||||
What's the best pattern for sharing this state app-wide across all
|
||||
pages and components, given the per-page interactivity setup?
|
||||
assertions:
|
||||
- type: "file_contains"
|
||||
path: "**/Program.cs"
|
||||
value: "CascadingValue"
|
||||
- type: "file_contains"
|
||||
path: "**/Program.cs"
|
||||
value: "NotifyChangedAsync"
|
||||
rubric:
|
||||
- "Uses CascadingValueSource<T> registered in DI (not <CascadingValue> in the layout) — explains this is required because CascadingValue in a static layout cannot reach interactive children"
|
||||
- "Calls NotifyChangedAsync when the theme changes — without this call, subscribers don't re-render even though the value object was mutated"
|
||||
- "Components consume the theme via [CascadingParameter] — same consumption pattern as CascadingValue but backed by DI-registered source"
|
||||
- "State is isolated per user — the CascadingValueSource registration is scoped (or effectively per-circuit), not singleton"
|
||||
- "Theme persists across enhanced navigations without page reload — the DI-registered source survives nav events within the same circuit"
|
||||
timeout: 900
|
||||
@@ -0,0 +1,94 @@
|
||||
scenarios:
|
||||
- name: "University course catalog with enrollment form"
|
||||
prompt: |
|
||||
Create a Blazor Web App for a university course catalog.
|
||||
|
||||
Requirements:
|
||||
- Browse departments and courses in a searchable, filterable list
|
||||
- View course detail pages (description, schedule, credits, instructor bio)
|
||||
- Students submit an enrollment request form (name, student ID, course selection) — standard HTML form, no wizards or real-time validation
|
||||
- Homepage shows featured courses and department links
|
||||
- No authentication — open to all students and visitors
|
||||
- Hosted on the university intranet with fast, reliable connectivity
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "dotnet new blazor"
|
||||
- type: "output_matches"
|
||||
pattern: "-int(eractivity)? None"
|
||||
- type: "output_not_contains"
|
||||
value: "blazorwasm"
|
||||
- type: "output_not_contains"
|
||||
value: "blazorserver"
|
||||
- type: "output_matches"
|
||||
pattern: "(Static SSR|no.interactive|None.*interactivity)"
|
||||
rubric:
|
||||
- "Correct template and configuration — single-project structure (no .Client project), Program.cs calls AddRazorComponents() without any AddInteractive* chains, App.razor does NOT set @rendermode on Routes or HeadOutlet"
|
||||
- "Recognizes that Static SSR uses standard HTML POST forms and enhanced navigation — no interactivity needed for a course catalog with enrollment"
|
||||
- "Documents the static rendering choice — explains why the app uses no interactive render mode"
|
||||
- "Plans the domain features — identifies the pages/components needed for browsing courses and submitting enrollments"
|
||||
timeout: 600
|
||||
|
||||
- name: "Recipe community with interactive ratings on static pages"
|
||||
prompt: |
|
||||
Create a new Blazor Web App project for a recipe sharing community.
|
||||
Run `dotnet new blazor` with the appropriate options, create the project structure,
|
||||
and implement the pages.
|
||||
|
||||
Requirements:
|
||||
- Browse recipes by category — read-only content pages
|
||||
- Each recipe page has a star-rating widget (click to rate 1–5 stars)
|
||||
- A "Submit Recipe" page with a form (title, ingredients, steps)
|
||||
- Homepage displays recipe categories (static content)
|
||||
- Most pages are read-only; only the rating widget and submit form need interactivity
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "dotnet new blazor"
|
||||
- type: "output_matches"
|
||||
pattern: "-int(eractivity)? Server"
|
||||
- type: "output_not_matches"
|
||||
pattern: "dotnet new blazor[^\\n]*-ai"
|
||||
- type: "output_not_contains"
|
||||
value: "blazorwasm"
|
||||
- type: "output_not_contains"
|
||||
value: "blazorserver"
|
||||
- type: "output_matches"
|
||||
pattern: "(per.page|per-page|opt.in)"
|
||||
rubric:
|
||||
- "Correct template and configuration — Program.cs registers AddInteractiveServerComponents() and AddInteractiveServerRenderMode(), App.razor does NOT set @rendermode on Routes so interactivity is per-page opt-in"
|
||||
- "Static pages (browse, homepage, recipe content) remain SSR for performance while interactive components (rating widget, comments) opt in with @rendermode InteractiveServer"
|
||||
- "Documents the per-page strategy — explains why most pages are static and only specific features opt in to interactivity"
|
||||
- "Differentiates which features need interactivity (rating widget, form submission) vs which can be static (content browsing)"
|
||||
timeout: 600
|
||||
|
||||
- name: "Global logistics tracking for worldwide users"
|
||||
prompt: |
|
||||
Create a Blazor Web App for a logistics company that tracks shipments.
|
||||
|
||||
Requirements:
|
||||
- Dispatchers view a list of shipments with status and destination
|
||||
- Customers check their shipment status and estimated delivery
|
||||
- Users are worldwide — round-trip latency to a single server is noticeable
|
||||
- The app must load fast on first visit, then feel snappy without server round-trips
|
||||
- Every page involves interactive controls
|
||||
- Staff and customers authenticate with individual accounts
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "dotnet new blazor"
|
||||
- type: "output_matches"
|
||||
pattern: "-int(eractivity)? Auto"
|
||||
- type: "output_matches"
|
||||
pattern: "(-ai|--all-interactive)"
|
||||
- type: "output_not_contains"
|
||||
value: "blazorwasm"
|
||||
- type: "output_not_contains"
|
||||
value: "blazorserver"
|
||||
- type: "output_matches"
|
||||
pattern: "(-au|--auth) Individual"
|
||||
- type: "output_matches"
|
||||
pattern: "(\\.Client|Auto.*mode|dual.*execution)"
|
||||
rubric:
|
||||
- "Correct template and configuration — two-project structure with .Client project, Server Program.cs registers both AddInteractiveServerComponents() and AddInteractiveWebAssemblyComponents(), interactive components live in .Client"
|
||||
- "Components cannot directly access server resources — all data through HTTP APIs, both server and client Program.cs register matching services, component code must not assume execution environment"
|
||||
- "Documents the Auto mode trade-offs — explains why this rendering strategy suits the requirements"
|
||||
- "Addresses the latency requirement — explains why Auto mode solves worldwide latency (Server first, then switches to WebAssembly for client-side speed)"
|
||||
timeout: 1200
|
||||
@@ -0,0 +1,117 @@
|
||||
scenarios:
|
||||
- name: "Recipe browser with resilient data loading"
|
||||
setup: &server_project
|
||||
commands:
|
||||
- "dotnet new blazor --interactivity server --no-https --force"
|
||||
prompt: |
|
||||
I have a Blazor Web App with Interactive Server (per-page).
|
||||
|
||||
I need an interactive /recipes/{CuisineId:int} page that calls a
|
||||
backend service to fetch and display recipe data for a given cuisine.
|
||||
It also accepts a ?sort= query parameter (newest or popular) that only
|
||||
controls display order.
|
||||
|
||||
Create a RecipeService with a GetByCuisineAsync(int cuisineId,
|
||||
CancellationToken cancellationToken) method that simulates a slow API
|
||||
call (Task.Delay(2000, cancellationToken)) and returns a list of
|
||||
recipes (Id, Title, Description, PrepTimeMinutes). Seed data for 3
|
||||
cuisines.
|
||||
|
||||
The data fetching should handle the full async lifecycle correctly —
|
||||
here are the specific behaviors I need:
|
||||
|
||||
1. If the user picks a different cuisine while the current one is still
|
||||
loading, stop loading the current cuisine and start loading the new one.
|
||||
Changing only the sort parameter should not trigger a data reload.
|
||||
|
||||
2. The first time the page loads (no data yet), show a loading placeholder.
|
||||
When navigating to a different cuisine after data is already on screen,
|
||||
keep the existing recipes visible and show a small "Refreshing…" indicator.
|
||||
When the new data arrives, swap it in and hide the indicator.
|
||||
|
||||
3. If something goes wrong — timeout, network error, whatever — show
|
||||
the user a friendly message and a Retry button so they can try again.
|
||||
If it's a timeout specifically, say so. Never show raw exception
|
||||
details to the user — log errors properly through ILogger.
|
||||
|
||||
4. If the user navigates away from the page, all ongoing work should stop
|
||||
immediately.
|
||||
assertions:
|
||||
- type: "file_exists"
|
||||
path: "**/Recipes.razor"
|
||||
- type: "file_contains"
|
||||
path: "**/Recipes.razor"
|
||||
value: "CancellationTokenSource"
|
||||
- type: "file_contains"
|
||||
path: "**/Recipes.razor"
|
||||
value: "IAsyncDisposable"
|
||||
- type: "file_contains"
|
||||
path: "**/Recipes.razor"
|
||||
value: "ILogger"
|
||||
rubric:
|
||||
- "Uses OnParametersSetAsync (not OnInitializedAsync) because the data depends on a route parameter that changes during navigation — guards with a tracked value to skip reloading when only the sort parameter changes"
|
||||
- "Cancels the previous in-flight request when CuisineId changes by cancelling the existing CancellationTokenSource and creating a new one — the token is captured into a local variable BEFORE the await to avoid races with a replaced CTS"
|
||||
- "On the first load (data is null), shows a loading placeholder. On subsequent loads (data already exists), keeps the existing data visible and shows an additional loading indicator — does NOT clear existing data on subsequent loads"
|
||||
- "Catches external OperationCanceledException using 'when (!cancellationToken.IsCancellationRequested)' — this means a timeout or external cancellation that the component did NOT trigger. Self-initiated cancellation (parameter change, disposal) is NOT caught because ComponentBase silently swallows it"
|
||||
- "Catches general Exception separately for non-cancellation errors — both catch blocks log via ILogger and set a user-facing error string that is a hardcoded generic message, NEVER exception.Message (PII risk)"
|
||||
- "Shows a Retry button when an error is set, wired to the load method so the user can attempt the operation again"
|
||||
- "Implements IAsyncDisposable that cancels and disposes the CancellationTokenSource — does NOT catch OperationCanceledException in DisposeAsync because ComponentBase handles self-cancellation automatically"
|
||||
timeout: 900
|
||||
|
||||
- name: "Real-time shipment tracker with Auto interactivity"
|
||||
setup:
|
||||
commands:
|
||||
- "dotnet new blazor --interactivity auto --no-https --force"
|
||||
prompt: |
|
||||
I have a Blazor Web App using Auto interactivity mode (per-page). Pages
|
||||
may run on Server first, then switch to WebAssembly.
|
||||
|
||||
I need an interactive /shipments/{TrackingNumber} page that tracks a
|
||||
package.
|
||||
|
||||
Create a ShipmentService interface with:
|
||||
- GetShipmentAsync(string trackingNumber, CancellationToken ct) — returns
|
||||
shipment details (TrackingNumber, Status, Location, EstimatedDelivery,
|
||||
and a list of StatusUpdate events with timestamps).
|
||||
|
||||
On the server side, implement a concrete ShipmentService that simulates a
|
||||
slow backend (Task.Delay(1500, ct)) and returns fake data. Seed data for
|
||||
3 tracking numbers.
|
||||
|
||||
Behaviors I need:
|
||||
|
||||
1. When the page loads, fetch the shipment details and display them. While
|
||||
loading, show a loading placeholder with the tracking number.
|
||||
|
||||
2. The component should work correctly regardless of whether it's running
|
||||
on Server or WebAssembly — the data access pattern must be the same in
|
||||
both environments.
|
||||
|
||||
3. If the user navigates to a different tracking number while data is
|
||||
loading, cancel the previous load and start fresh for the new tracking
|
||||
number.
|
||||
|
||||
4. If something goes wrong — timeout, network error, anything — show the
|
||||
user a friendly message and let them retry. If it's a timeout, say so.
|
||||
Don't show raw exception details. Log errors through ILogger.
|
||||
|
||||
5. When the user leaves the page, all pending requests must stop
|
||||
immediately.
|
||||
assertions:
|
||||
- type: "file_exists"
|
||||
path: "**/*.Client/**/*.razor"
|
||||
- type: "file_contains"
|
||||
path: "**/*.Client/**/Program.cs"
|
||||
value: "HttpClient"
|
||||
- type: "file_contains"
|
||||
path: "**/*.razor"
|
||||
value: "CancellationTokenSource"
|
||||
- type: "file_contains"
|
||||
path: "**/*.razor"
|
||||
value: "IAsyncDisposable"
|
||||
rubric:
|
||||
- "Uses OnParametersSetAsync because the route parameter changes during navigation — cancels previous in-flight requests when TrackingNumber changes"
|
||||
- "Handles errors with user-facing messages and a retry mechanism — logs through ILogger and never displays exception.Message"
|
||||
- "Implements IAsyncDisposable that cancels and disposes the CancellationTokenSource — stops all pending requests on disposal"
|
||||
- "The data access pattern works regardless of execution environment — component depends on an abstraction, not a concrete server-only service"
|
||||
timeout: 900
|
||||
@@ -0,0 +1,306 @@
|
||||
scenarios:
|
||||
- name: "Project management Kanban board"
|
||||
prompt: |
|
||||
Build a Blazor page for a project management Kanban board. This is a complex
|
||||
multi-section layout. Here are the service contracts
|
||||
already registered in DI:
|
||||
|
||||
```csharp
|
||||
public record TaskItem(int Id, string Title, string Assignee, string Priority, DateTime DueDate, string Status);
|
||||
|
||||
public interface ITaskService
|
||||
{
|
||||
Task<List<TaskItem>> GetAllTasksAsync();
|
||||
Task<TaskItem> CreateTaskAsync(string title, string assignee, string priority, DateTime dueDate);
|
||||
Task UpdateTaskStatusAsync(int taskId, string newStatus);
|
||||
}
|
||||
```
|
||||
|
||||
The page should have:
|
||||
- A summary stats bar at the top showing: total task count, count per column (To Do, In Progress, Done),
|
||||
and a count of overdue tasks (DueDate < today)
|
||||
- A search bar that filters visible tasks by title (client-side filtering)
|
||||
- Three columns side by side: "To Do", "In Progress", "Done"
|
||||
- Each column header shows the column name and the number of tasks in it
|
||||
- Each column displays task cards showing: title, assignee name, a priority badge (High = red,
|
||||
Medium = yellow, Low = green CSS class), and the due date formatted as "MMM dd"
|
||||
- Each task card has "Move Left" and "Move Right" buttons to move the task to the adjacent column
|
||||
(hide the button if there's no column in that direction). Moving a task calls UpdateTaskStatusAsync
|
||||
- Each column has an "Add Task" button at the bottom. Clicking it reveals an inline form within that
|
||||
column with inputs for title, assignee, priority (dropdown: High/Medium/Low), and due date.
|
||||
A "Save" button calls CreateTaskAsync and a "Cancel" button hides the form
|
||||
- The page loads all tasks on initialization and handles loading/error states
|
||||
|
||||
Route: `/board`
|
||||
setup: &blazor_project
|
||||
commands:
|
||||
- "dotnet new blazor --interactivity Server --no-https"
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "[Parameter]"
|
||||
- type: "output_contains"
|
||||
value: "EventCallback"
|
||||
- type: "output_matches"
|
||||
pattern: "(ITaskService|TaskItem)"
|
||||
rubric:
|
||||
- "The board is organized into focused, reusable components — task cards, columns, and the stats bar are separate from the page itself"
|
||||
- "State changes in child components propagate correctly to the parent and other siblings — e.g., moving a task updates the column counts"
|
||||
- "Handles loading and error states with conditional rendering (@if/@else)"
|
||||
timeout: 1800
|
||||
|
||||
- name: "E-commerce product catalog with filters and pagination"
|
||||
prompt: |
|
||||
Build a Blazor page for browsing a product catalog. Here are the service contracts
|
||||
already registered in DI:
|
||||
|
||||
```csharp
|
||||
public record Product(int Id, string Name, decimal Price, string Category, double Rating, bool InStock, string ImageUrl);
|
||||
public record ProductFilter(List<string>? Categories, decimal? MinPrice, decimal? MaxPrice, bool InStockOnly);
|
||||
public record PagedResult<T>(List<T> Items, int TotalCount, int Page, int PageSize);
|
||||
|
||||
public interface IProductService
|
||||
{
|
||||
Task<PagedResult<Product>> SearchAsync(ProductFilter filter, string sortBy, int page, int pageSize);
|
||||
Task<List<string>> GetCategoriesAsync();
|
||||
}
|
||||
```
|
||||
|
||||
The page layout should have:
|
||||
- A left sidebar (about 25% width) with filter controls:
|
||||
- Category checkboxes (loaded from GetCategoriesAsync)
|
||||
- Min price and max price number inputs
|
||||
- An "In Stock Only" checkbox toggle
|
||||
- An "Apply Filters" button that triggers a new search
|
||||
- A "Clear Filters" button that resets all filters to defaults
|
||||
- A main content area (about 75% width) with:
|
||||
- A toolbar row containing: a sort dropdown (options: "Price: Low to High", "Price: High to Low",
|
||||
"Name: A-Z", "Rating: Highest"), and a "Showing X–Y of Z products" label
|
||||
- A product grid displaying product cards, each showing: a placeholder image div with the product name
|
||||
as alt text, the product name, the price formatted as currency, a star rating display (e.g., "★★★★☆"
|
||||
for 4/5), an "Out of Stock" badge if not in stock, and an "Add to Cart" button (disabled when out of stock)
|
||||
- When a product card is clicked (not the Add to Cart button), the parent should be notified via
|
||||
EventCallback so it can navigate to a detail page
|
||||
- Pagination controls at the bottom: "Previous" button (disabled on first page), numbered page buttons,
|
||||
"Next" button (disabled on last page)
|
||||
- The page loads the first page of products and the category list on initialization
|
||||
- Handle loading, empty results, and error states
|
||||
|
||||
Route: `/catalog`
|
||||
setup: *blazor_project
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "[Parameter]"
|
||||
- type: "output_contains"
|
||||
value: "EventCallback"
|
||||
- type: "output_matches"
|
||||
pattern: "(IProductService|ProductFilter|PagedResult)"
|
||||
rubric:
|
||||
- "The catalog uses a multi-component architecture — product cards, filter sidebar, and pagination are separate reusable units"
|
||||
- "Child components communicate changes back to the parent via defined interfaces — e.g., filter sidebar notifies the page when filters change, pagination notifies on page change"
|
||||
- "Filtering, sorting, and pagination all work together — changing filters resets to page 1, changing sort re-fetches"
|
||||
- "Handles loading, empty, and error states with conditional rendering"
|
||||
timeout: 1800
|
||||
|
||||
- name: "Multi-step job application wizard"
|
||||
prompt: |
|
||||
Build a Blazor page for a multi-step job application form. This has many
|
||||
distinct visual sections and shared state. Here are the service contracts already registered in DI:
|
||||
|
||||
```csharp
|
||||
public record JobApplication(PersonalInfo Personal, WorkHistory Work, List<Reference> References, string CoverLetter);
|
||||
public record PersonalInfo(string FullName, string Email, string Phone, string Address);
|
||||
public record WorkHistory(List<WorkEntry> Entries);
|
||||
public record WorkEntry(string Company, string Title, DateTime StartDate, DateTime? EndDate, string Description);
|
||||
public record Reference(string Name, string Relationship, string Email, string Phone);
|
||||
|
||||
public interface IApplicationService
|
||||
{
|
||||
Task<int> SubmitApplicationAsync(JobApplication application);
|
||||
Task<bool> ValidateEmailAsync(string email);
|
||||
}
|
||||
```
|
||||
|
||||
The page should have:
|
||||
|
||||
**Step indicator bar:**
|
||||
- Shows all steps: "Personal Info" → "Work History" → "References" → "Cover Letter" → "Review & Submit"
|
||||
- The current step is highlighted, completed steps show a checkmark, future steps are dimmed
|
||||
- Users can click a completed step to go back and edit it
|
||||
|
||||
**Step 1 — Personal Info:**
|
||||
- Inputs for full name, email, phone, and address
|
||||
- Email is validated via ValidateEmailAsync when the user moves to the next step
|
||||
- A "Next" button that validates required fields before advancing
|
||||
|
||||
**Step 2 — Work History:**
|
||||
- A dynamic list of work entries — start with one empty entry
|
||||
- Each entry has: company name, job title, start date, end date (optional, checkbox for "current"), description
|
||||
- "Add Another Position" button to add more entries
|
||||
- "Remove" button on each entry (except if it's the only one)
|
||||
- "Back" and "Next" buttons
|
||||
|
||||
**Step 3 — References:**
|
||||
- Space for 2-3 references, each with: name, relationship, email, phone
|
||||
- "Add Reference" button (max 3)
|
||||
- "Back" and "Next" buttons
|
||||
|
||||
**Step 4 — Cover Letter:**
|
||||
- A large textarea for the cover letter
|
||||
- A character count display (e.g., "342 / 2000 characters")
|
||||
- "Back" and "Next" buttons
|
||||
|
||||
**Step 5 — Review & Submit:**
|
||||
- Read-only summary of all entered data organized by section
|
||||
- An "Edit" link next to each section that jumps back to that step
|
||||
- A "Submit Application" button that calls SubmitApplicationAsync
|
||||
- After submission, show a confirmation with the application ID
|
||||
|
||||
Route: `/apply`
|
||||
setup: *blazor_project
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "[Parameter]"
|
||||
- type: "output_contains"
|
||||
value: "EventCallback"
|
||||
- type: "output_matches"
|
||||
pattern: "(IApplicationService|JobApplication)"
|
||||
rubric:
|
||||
- "The wizard is decomposed so each step lives in its own component — navigation flows between them rather than toggling visibility in one file"
|
||||
- "Step data is passed down via parameters and changes flow up via callbacks — steps don't share or mutate parent state directly"
|
||||
- "Navigation between steps validates before advancing — prevents skipping required fields"
|
||||
- "The review step displays a read-only summary of all steps and supports going back to edit a specific step"
|
||||
timeout: 2400
|
||||
|
||||
- name: "Application settings page with nested tab panels"
|
||||
prompt: |
|
||||
Build a Blazor page for an application settings dashboard. This has multiple
|
||||
nested sections. Here are the service contracts
|
||||
already registered in DI:
|
||||
|
||||
```csharp
|
||||
public record UserProfile(string DisplayName, string Email, string AvatarUrl, string Bio);
|
||||
public record NotificationPrefs(bool EmailNotifications, bool PushNotifications, bool WeeklyDigest, List<string> MutedChannels);
|
||||
public record SecuritySettings(bool TwoFactorEnabled, DateTime? LastPasswordChange, List<ActiveSession> Sessions);
|
||||
public record ActiveSession(string Id, string Device, string Location, DateTime LastActive);
|
||||
public record AppearanceSettings(string Theme, string FontSize, string Language, bool CompactMode);
|
||||
|
||||
public interface ISettingsService
|
||||
{
|
||||
Task<UserProfile> GetProfileAsync();
|
||||
Task SaveProfileAsync(UserProfile profile);
|
||||
Task<NotificationPrefs> GetNotificationPrefsAsync();
|
||||
Task SaveNotificationPrefsAsync(NotificationPrefs prefs);
|
||||
Task<SecuritySettings> GetSecuritySettingsAsync();
|
||||
Task ChangePasswordAsync(string currentPassword, string newPassword);
|
||||
Task RevokeSessionAsync(string sessionId);
|
||||
Task<AppearanceSettings> GetAppearanceAsync();
|
||||
Task SaveAppearanceAsync(AppearanceSettings settings);
|
||||
}
|
||||
```
|
||||
|
||||
The page should have:
|
||||
|
||||
**Left sidebar navigation (vertical tabs):**
|
||||
- Four sections: "Profile", "Notifications", "Security", "Appearance"
|
||||
- The active section is highlighted
|
||||
- Clicking a section loads its content in the main area
|
||||
|
||||
**Profile section:**
|
||||
- Editable fields: display name, email, bio (textarea)
|
||||
- An avatar preview area with the current URL displayed
|
||||
- A "Save Changes" button — shows a success toast/message on save
|
||||
- A "Discard Changes" button that reverts to the last saved state
|
||||
|
||||
**Notifications section:**
|
||||
- Toggle switches for: email notifications, push notifications, weekly digest
|
||||
- A list of channels with checkboxes to mute/unmute each
|
||||
- A "Save Preferences" button
|
||||
|
||||
**Security section:**
|
||||
- A "Change Password" expandable panel with: current password, new password, confirm password fields and a "Update Password" button
|
||||
- Two-factor authentication toggle with status indicator
|
||||
- An "Active Sessions" table showing: device, location, last active time, and a "Revoke" button for each session (except the current one)
|
||||
- A confirmation dialog before revoking a session
|
||||
|
||||
**Appearance section:**
|
||||
- Theme selector: radio buttons for "Light", "Dark", "System"
|
||||
- Font size selector: "Small", "Medium", "Large"
|
||||
- Language dropdown
|
||||
- Compact mode toggle
|
||||
- A live preview area that shows how the selected options look
|
||||
- A "Save" button
|
||||
|
||||
Route: `/settings`
|
||||
setup: *blazor_project
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "[Parameter]"
|
||||
- type: "output_contains"
|
||||
value: "EventCallback"
|
||||
- type: "output_matches"
|
||||
pattern: "(ISettingsService|UserProfile|SecuritySettings)"
|
||||
rubric:
|
||||
- "Each settings section (Profile, Notifications, Security, Appearance) is its own component — not all inlined in the page"
|
||||
- "Switching tabs loads the correct section content without a full page reload"
|
||||
- "Handles the save/discard flow with appropriate UI feedback (success message, disabled buttons during save)"
|
||||
timeout: 2400
|
||||
|
||||
- name: "Team chat interface with message threads"
|
||||
prompt: |
|
||||
Build a Blazor page for a team messaging app. This is a complex multi-panel
|
||||
layout with a sidebar, main area, and collapsible thread panel. Here are the service contracts
|
||||
already registered in DI:
|
||||
|
||||
```csharp
|
||||
public record Channel(int Id, string Name, string Description, int UnreadCount);
|
||||
public record Message(int Id, int ChannelId, string Author, string Content, DateTime SentAt, int? ReplyToId, List<Reaction> Reactions);
|
||||
public record Reaction(string Emoji, List<string> Users);
|
||||
public record ThreadSummary(int RootMessageId, int ReplyCount, DateTime LastReplyAt);
|
||||
|
||||
public interface IChatService
|
||||
{
|
||||
Task<List<Channel>> GetChannelsAsync();
|
||||
Task<List<Message>> GetMessagesAsync(int channelId, int skip, int take);
|
||||
Task<Message> SendMessageAsync(int channelId, string content, int? replyToId);
|
||||
Task<List<Message>> GetThreadAsync(int rootMessageId);
|
||||
Task AddReactionAsync(int messageId, string emoji);
|
||||
Task MarkChannelReadAsync(int channelId);
|
||||
}
|
||||
```
|
||||
|
||||
The page layout should have:
|
||||
|
||||
**Left sidebar (channel list, about 20% width):**
|
||||
- A list of channels, each showing: channel name (with # prefix), description snippet, and an unread count badge
|
||||
- The active channel is highlighted
|
||||
- Clicking a channel loads its messages in the main area and marks it as read
|
||||
|
||||
**Main message area (about 55% width, or 80% if no thread is open):**
|
||||
- A header showing the current channel name and description
|
||||
- A scrollable message list showing messages for the selected channel
|
||||
- Each message displays: author name, timestamp (relative — "2 min ago"), message content, and a row of reaction emoji badges (each showing emoji + count)
|
||||
- Hovering a message reveals action buttons: "Reply" (opens thread panel), "React" (shows emoji picker with 5-6 common emojis)
|
||||
- Messages that are thread starters show a "N replies" link below them
|
||||
- A message input area at the bottom: a textarea and a "Send" button
|
||||
- Load older messages on scroll-to-top (pagination via skip/take)
|
||||
|
||||
**Right panel (thread view, about 25% width, shown when a thread is open):**
|
||||
- Header: "Thread" with the original message quoted and a close button
|
||||
- A scrollable list of reply messages in the thread
|
||||
- A reply input area at the bottom (textarea + "Reply" button)
|
||||
- Closing the thread panel widens the main message area back
|
||||
|
||||
Route: `/chat`
|
||||
setup: *blazor_project
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "[Parameter]"
|
||||
- type: "output_contains"
|
||||
value: "EventCallback"
|
||||
- type: "output_matches"
|
||||
pattern: "(IChatService|Message|Channel)"
|
||||
rubric:
|
||||
- "The chat interface is decomposed into focused components — channel list, message area, thread panel, and message input each have clear responsibilities"
|
||||
- "The message input component is reused in both the main area and thread panel"
|
||||
- "Channel selection, sending messages, and opening threads all work correctly — selecting a channel loads its messages, opening a thread shows replies"
|
||||
timeout: 2400
|
||||
@@ -0,0 +1,68 @@
|
||||
scenarios:
|
||||
- name: "Equipment inventory loaded once"
|
||||
prompt: |
|
||||
I have an existing Blazor Web App with Interactive Server (per-page).
|
||||
|
||||
I need an interactive /inventory page that shows a table of equipment items
|
||||
(Id, Name, Location, LastInspected date). The data comes from an EquipmentService
|
||||
you should create (just return fake data, 5-10 items).
|
||||
|
||||
The page should show "Loading inventory..." until data is ready, then display
|
||||
the table. The important thing is that when the page first loads I see the HTML
|
||||
instantly (fast initial paint), but the equipment data should NOT be fetched
|
||||
twice — once during the initial server render and again when the page becomes
|
||||
interactive. It should only call the service once and carry that data across.
|
||||
assertions:
|
||||
- type: "file_exists"
|
||||
path: "**/Inventory.razor"
|
||||
- type: "file_contains"
|
||||
path: "**/Inventory.razor"
|
||||
value: "PersistentState"
|
||||
- type: "file_contains"
|
||||
path: "**/Inventory.razor"
|
||||
value: "InteractiveServer"
|
||||
- type: "file_contains"
|
||||
path: "**/Inventory.razor"
|
||||
value: "??="
|
||||
rubric:
|
||||
- "Data persists across the prerender-to-interactive handoff — uses state persistence ([PersistentState] or PersistentComponentState) with ??= pattern to avoid duplicate data fetches"
|
||||
- "Component correctly shows loading state when data is null, then displays the equipment table when loaded"
|
||||
- "Does NOT disable prerendering — preserves the fast initial HTML response while avoiding duplicate loads"
|
||||
- "Creates a proper EquipmentService registered in DI, not inline data generation"
|
||||
timeout: 600
|
||||
|
||||
|
||||
- name: "Notifications page with live polling"
|
||||
prompt: |
|
||||
I have an existing Blazor Web App with Interactive Server (per-page).
|
||||
|
||||
I need an interactive /notifications page that:
|
||||
- Loads an initial set of notifications from a NotificationService (create it with fake data)
|
||||
- After the page becomes interactive, starts a background polling loop that
|
||||
checks for new notifications every 5 seconds and updates the UI
|
||||
- Shows a notification count badge and a list of notification messages
|
||||
|
||||
Important constraints:
|
||||
- The initial notifications should appear in the fast server-rendered HTML
|
||||
and NOT be fetched again when the page becomes interactive
|
||||
- The polling loop must not run during the initial server render — only after
|
||||
the page is live and interactive in the browser
|
||||
- When the user navigates away, the polling should stop cleanly (no leaked tasks)
|
||||
assertions:
|
||||
- type: "file_exists"
|
||||
path: "**/Notifications.razor"
|
||||
- type: "file_contains"
|
||||
path: "**/Notifications.razor"
|
||||
value: "RendererInfo.IsInteractive"
|
||||
- type: "file_contains"
|
||||
path: "**/Notifications.razor"
|
||||
value: "PersistentState"
|
||||
- type: "file_contains"
|
||||
path: "**/Notifications.razor"
|
||||
value: "InteractiveServer"
|
||||
rubric:
|
||||
- "Guards the polling loop so it only runs when the component is interactive — not during prerendering"
|
||||
- "Persists initial notification data across the prerender-to-interactive handoff so the service is not called twice"
|
||||
- "Polling uses an async loop with CancellationTokenSource, cancelled in DisposeAsync — not System.Threading.Timer"
|
||||
- "Component implements IAsyncDisposable and cancels the polling token in DisposeAsync to prevent resource leaks"
|
||||
timeout: 600
|
||||
@@ -0,0 +1,158 @@
|
||||
scenarios:
|
||||
- name: "Auto-saving notepad that survives page reloads"
|
||||
prompt: |
|
||||
I need a `Notepad` component for my Blazor Web App.
|
||||
|
||||
Requirements:
|
||||
- A textarea where the user can type notes
|
||||
- Content is automatically saved to the browser every 30 seconds
|
||||
- When the user returns to the page (or reloads), their last saved content is restored
|
||||
- A "Last saved at HH:MM:SS" label that updates after each save
|
||||
- If the user has unsaved changes and tries to close the browser tab, show the browser's
|
||||
built-in "Leave site?" confirmation dialog
|
||||
- A "Clear" button that erases both the textarea and the saved data
|
||||
- A "Save now" button for manual saves
|
||||
setup: &blazor_project
|
||||
commands:
|
||||
- "dotnet new blazor --interactivity Server --no-https"
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "OnAfterRenderAsync"
|
||||
- type: "output_contains"
|
||||
value: "IAsyncDisposable"
|
||||
- type: "output_matches"
|
||||
pattern: "(export function|export \\{)"
|
||||
rubric:
|
||||
- "Uses a collocated .razor.js file with exported functions — not a global script tag or window-level globals"
|
||||
- "Initializes interop in OnAfterRenderAsync(firstRender), not in OnInitializedAsync — JS is unavailable during prerendering"
|
||||
- "Restores saved content on first interactive render so the user sees their previous notes immediately"
|
||||
- "Batches related browser operations into single JS calls where possible — e.g., save content + timestamp together instead of separate interop calls for each"
|
||||
- "Registers and removes the beforeunload listener properly — adds on setup, removes on dispose"
|
||||
- "Manages the auto-save interval in JS and clears it on dispose — no dangling timers"
|
||||
- "Implements IAsyncDisposable and catches JSDisconnectedException so disposal doesn't throw when the circuit is already gone"
|
||||
- "Uses InvokeVoidAsync when no return value is needed"
|
||||
- "Does not use fire-and-forget (unawaited ValueTask) — all JS calls are properly awaited"
|
||||
- "The clear function removes stored data and resets the beforeunload guard in one operation"
|
||||
- "Uses sessionStorage (or localStorage) for persistence, the beforeunload event for the unsaved-changes prompt, and setInterval/clearInterval for the auto-save timer"
|
||||
timeout: 600
|
||||
|
||||
- name: "User activity tracker that detects idle timeout"
|
||||
prompt: |
|
||||
I need a `UserActivityTracker` component for my Blazor Web App.
|
||||
|
||||
Requirements:
|
||||
- Monitors user activity on the page (mouse movement, key presses, clicks, scrolling)
|
||||
- After a configurable period of inactivity, triggers an `EventCallback OnIdle` to notify the parent
|
||||
- When the user becomes active again after being idle, triggers `EventCallback OnActive`
|
||||
- The idle timeout duration is set via `[Parameter] int TimeoutSeconds` (default 300)
|
||||
- Wraps child content — can be placed around any section of the app
|
||||
- After the component is removed from the page, no leftover timers or event handlers should remain
|
||||
setup: *blazor_project
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "OnAfterRenderAsync"
|
||||
- type: "output_contains"
|
||||
value: "DotNetObjectReference"
|
||||
- type: "output_contains"
|
||||
value: "IAsyncDisposable"
|
||||
- type: "output_matches"
|
||||
pattern: "(export function|export \\{)"
|
||||
rubric:
|
||||
- "Uses a collocated .razor.js file with exported functions — not global window functions or inline scripts"
|
||||
- "Registers activity event listeners (mousemove, keydown, click, scroll) in OnAfterRenderAsync(firstRender) — not during prerendering"
|
||||
- "Uses DotNetObjectReference so JS can call back into .NET for idle/active transitions and disposes it in DisposeAsync"
|
||||
- "Implements IAsyncDisposable and catches JSDisconnectedException in DisposeAsync"
|
||||
- "Manages the idle timeout timer in JavaScript using setTimeout/clearTimeout — not via .NET Task.Delay"
|
||||
- "Cleans up all event listeners and timers in the JS dispose function — no dangling handlers after component removal"
|
||||
timeout: 600
|
||||
|
||||
|
||||
- name: "Responsive layout that adapts to screen size"
|
||||
prompt: |
|
||||
My Blazor Web App needs to adapt its layout based on the browser viewport width. I need:
|
||||
|
||||
1. A `ScreenSizeProvider` component that wraps child content and detects the current
|
||||
viewport category:
|
||||
- `Mobile` (width < 768px)
|
||||
- `Tablet` (768px ≤ width < 1024px)
|
||||
- `Desktop` (width ≥ 1024px)
|
||||
|
||||
2. The current category should be available to all child components as a cascading value
|
||||
|
||||
3. When the user resizes their browser, the category should update in real time
|
||||
|
||||
4. The default category (before detection runs) should be `Desktop`
|
||||
|
||||
Usage:
|
||||
```razor
|
||||
<ScreenSizeProvider>
|
||||
<MyNavigation /> @* can read the cascading ScreenSize value *@
|
||||
<MyContent />
|
||||
</ScreenSizeProvider>
|
||||
```
|
||||
setup: *blazor_project
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "CascadingValue"
|
||||
- type: "output_contains"
|
||||
value: "OnAfterRenderAsync"
|
||||
- type: "output_contains"
|
||||
value: "IAsyncDisposable"
|
||||
- type: "output_matches"
|
||||
pattern: "(export function|export \\{)"
|
||||
rubric:
|
||||
- "Provides the screen size category to children via CascadingValue — child components can consume it with [CascadingParameter]"
|
||||
- "Uses a collocated .razor.js module with exported functions — not global scripts"
|
||||
- "Sets up matchMedia listeners or resize listener in OnAfterRenderAsync(firstRender) — not in OnInitializedAsync"
|
||||
- "Defaults to Desktop (or a sensible fallback) during prerendering before JavaScript is available"
|
||||
- "Updates the category in real time when the browser is resized — uses event-based detection, not polling"
|
||||
- "Uses DotNetObjectReference for the JS resize/matchMedia callback and disposes it in DisposeAsync"
|
||||
- "The [JSInvokable] callback wraps state changes in InvokeAsync(StateHasChanged)"
|
||||
- "Implements IAsyncDisposable and catches JSDisconnectedException in DisposeAsync"
|
||||
- "Removes matchMedia/resize event listeners in the JS dispose function — no dangling listeners"
|
||||
- "Does not call JS interop during prerendering — guards all JS calls behind firstRender or a flag"
|
||||
- "Uses window.matchMedia or the resize event for viewport detection — not polling or server-side user-agent sniffing"
|
||||
timeout: 600
|
||||
|
||||
- name: "Infinite scroll list using IntersectionObserver"
|
||||
prompt: |
|
||||
I need an `InfiniteScrollList` component for my Blazor Web App that loads more items
|
||||
as the user scrolls.
|
||||
|
||||
Requirements:
|
||||
- Displays items in a scrollable container
|
||||
- When the user scrolls near the bottom, automatically triggers loading of more items
|
||||
- Shows a loading spinner while fetching
|
||||
- Stops triggering when there are no more items (parent signals this via a parameter)
|
||||
- The component is generic — it accepts a `RenderFragment<TItem>` for rendering each item
|
||||
- Uses an efficient browser API for scroll detection (not scroll event listeners)
|
||||
|
||||
Usage:
|
||||
```razor
|
||||
<InfiniteScrollList Items="products" HasMore="hasMore"
|
||||
OnLoadMore="LoadNextPage" Context="product">
|
||||
<div class="product-card">@product.Name — @product.Price</div>
|
||||
</InfiniteScrollList>
|
||||
```
|
||||
setup: *blazor_project
|
||||
assertions:
|
||||
- type: "output_contains"
|
||||
value: "IntersectionObserver"
|
||||
- type: "output_contains"
|
||||
value: "OnAfterRenderAsync"
|
||||
- type: "output_contains"
|
||||
value: "IAsyncDisposable"
|
||||
- type: "output_contains"
|
||||
value: "DotNetObjectReference"
|
||||
rubric:
|
||||
- "Uses a collocated .razor.js file with exported functions — not a global script"
|
||||
- "Creates IntersectionObserver in OnAfterRenderAsync(firstRender) — not during prerendering"
|
||||
- "Uses a sentinel element (observed by IntersectionObserver) at the bottom of the list to detect when to load more"
|
||||
- "Uses DotNetObjectReference to receive the intersection callback from JS and disposes it in DisposeAsync"
|
||||
- "The [JSInvokable] callback is public and wraps state changes in InvokeAsync(StateHasChanged)"
|
||||
- "Calls observer.disconnect() in the JS dispose function — no dangling observers"
|
||||
- "Implements IAsyncDisposable and catches JSDisconnectedException in DisposeAsync"
|
||||
- "Uses ElementReference for the sentinel element — not a string ID"
|
||||
- "Respects the HasMore parameter — does not trigger loads or observe the sentinel when there are no more items"
|
||||
- "Uses IntersectionObserver (not scroll event listeners) for efficient visibility detection"
|
||||
timeout: 600
|
||||
Reference in New Issue
Block a user