Author Component
.NET (Microsoft)OfficialPublished by .NET (Microsoft)
- Skill
- Guidelines
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured approach to building Blazor components, focusing on correct architectural patterns, lifecycle management, and asynchronous programming. It emphasizes maintaining the Blazor synchronization context, proper data flow using Parameters and EventCallbacks, and efficient component decomposition. The guidelines cover best practices for handling component state, implementing IAsyncDisposable, and avoiding common pitfalls like thread-pool escapes or incorrect parameter usage.
What it does
- Defines standards for data flow using Parameters and EventCallbacks.
- Provides patterns for component lifecycle management and asynchronous operations.
- Outlines rules for component decomposition and sibling extraction.
- Specifies implementation details for IAsyncDisposable and resource cleanup.
- Enforces architectural constraints to prevent deadlocks and synchronization issues.
- Guides the use of RenderFragments and generic component templates.
When to use it
- When creating new Blazor components that require custom logic.
- When refactoring complex components into smaller, maintainable pieces.
- When implementing asynchronous data loading or event handling within a component.
- When ensuring components correctly handle disposal of subscriptions or timers.
- When standardizing code-behind or single-file component structures.
Advantages
- Prevents common Blazor-specific runtime errors and deadlocks.
- Promotes clean, modular component architecture.
- Ensures consistent handling of asynchronous tasks and UI synchronization.
- Reduces technical debt by enforcing established best practices.
- Improves component performance through efficient diffing and state management.
Usage examples
- “Create a reusable Card component that accepts a title and a child content template.”
- Generates a .razor file using [Parameter] for the title and a RenderFragment for the ChildContent, following the recommended data flow and structure.
- “How should I handle an async timer in a Blazor component?”
- Provides a pattern using Task.Delay and CancellationTokenSource within the component lifecycle, ensuring proper disposal via IAsyncDisposable.
- “Refactor this large component that has too much logic in the @code block.”
- Suggests splitting the component into a .razor file and a .razor.cs code-behind file, or extracting sub-components if the logic involves independent blocks.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/dotnet/author-component/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip author-component.zip -d .agents/skills
Documentation by .NET (Microsoft)
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 templateUse @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 failureMutate
[Parameter]— copy to private field inOnParametersSetAction/Funcfor events — useEventCallback<T>Task.Run/.Result/.Wait()/Timer for debounce — deadlock or thread-pool escapeInline
styleattributes — use CSS classes ordata-*attributescatch { throw; }— usewhenguard or let exceptions propagateGold-plating: ARIA, wrapper divs, accessibility features not requested
_ = InvokeAsync(...)— swallows exceptions; useasync void+DispatchExceptionAsync
Files
- ATTRIBUTION.mdAdded by AutoGineer845 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SKILL.md3.3 KiB
- references/async-programming-rules.md6.8 KiB
- references/breaking-down-components.md3.2 KiB
- references/component-disposal.md2.7 KiB
references/async-programming-rules.md
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).
// 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 |
// 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:
Intermediate updates between multiple awaits:
private async Task ProcessSteps()
{
status = "Step 1...";
await Step1Async();
status = "Step 2...";
StateHasChanged(); // intermediate update
await Step2Async();
}External events (timer, C# event, WebSocket) via
InvokeAsync:
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):
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:
// 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:
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:
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
// 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:
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:
// 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
// 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:
@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;
}
}Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- .NET (Microsoft)
- Publisher
- .NET (Microsoft) (Official)
- Source
- dotnet/skills · plugins/dotnet-blazor/skills/author-component · commit 96c3d68
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by .NET (Microsoft) or .NET (Microsoft).
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download author-component.zip