author-component
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),
Install
npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-blazor/skills/author-component
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart
git clone https://github.com/dotnet/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dotnet/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Author Blazor Component
Core Rules
- Data flows down via
[Parameter]. Events flow up viaEventCallback<T>(neverAction/Func). - Never mutate
[Parameter]properties. Copy to a private field inOnParametersSet. - Use
[Parameter] public T Prop { get; set; }— neverrequiredorinit(causes BL0007). - Use
[EditorRequired]for required parameters. - Handle all states: loading, empty, loaded, error — each with
@if/@else. - Use
@keyon repeated elements in loops for efficient diffing. - Use
IReadOnlyList<T>(notIEnumerable<T>) for collection parameters.
RenderFragment & Generics
[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:
.razorwith@codeblock when logic < ~50 lines. - Code-behind:
.razor+.razor.cswithpartial classwhen 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
awaitevery 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 useSystem.Threading.TimerorSystem.Timers.Timer. - Polling: Loop in
OnInitializedAsyncwithawait Task.Delay(interval, token)— stays on sync context. - External events (
Action<T>): Useasync voidhandler +await InvokeAsync(() => { state++; StateHasChanged(); })+catch→DispatchExceptionAsync. Never_ = InvokeAsync(...). - Cancel CTS in
DisposeAsync. Don't catchObjectDisposedException— use CTS cancellation.
Don'ts
required/initon[Parameter]— runtime failure- Mutate
[Parameter]— copy to private field inOnParametersSet Action/Funcfor events — useEventCallback<T>Task.Run/.Result/.Wait()/Timer for debounce — deadlock or thread-pool escape- Inline
styleattributes — use CSS classes ordata-*attributes catch { throw; }— usewhenguard or let exceptions propagate- Gold-plating: ARIA, wrapper divs, accessibility features not requested
_ = InvokeAsync(...)— swallows exceptions; useasync void+DispatchExceptionAsync
Files (skills)
-
references
-
async-programming-rules.md 6.8 KB
# 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; } } ``` -
breaking-down-components.md 3.2 KB
# 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 { ... });` -
component-disposal.md 2.7 KB
# 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.
-
-
SKILL.md 3.3 KB
--- 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`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.