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:
Javier Calvarro Nelson
2026-05-25 18:25:27 +02:00
committed by GitHub
parent da5e63e878
commit 198e58c983
37 changed files with 4667 additions and 0 deletions
+5
View File
@@ -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",
+5
View File
@@ -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",
+4
View File
@@ -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
+5
View File
@@ -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",
+1
View File
@@ -430,3 +430,4 @@ vally-results/
# Roslyn / C# language server cache files
*.lscache
.dotnet/
+1
View File
@@ -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
+6
View File
@@ -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&lt;T&gt; 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.
@@ -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.
@@ -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.
@@ -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 15 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 XY 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