Generated banner for Author Component
Generated logo for Author Component

Author Component

.NET (Microsoft)OfficialPublished by .NET (Microsoft)

  • Skill
  • Guidelines
  • Software development
  • MIT
  • No issues found

From the author

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).
— the description by .NET (Microsoft)

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/dotnet/author-component/download
  2. Unzip 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 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

[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

Files

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:

ForbiddenWhy
Thread.Start / new ThreadEscapes sync context
Task.RunOffloads to thread-pool; StateHasChanged throws
.Result / .Wait()Deadlocks sync context
Task.ContinueWithContinuation runs outside sync context
Channel<T>, BlockingCollection<T>, concurrent collectionsUnnecessary — 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:

  1. Intermediate updates between multiple awaits:

private async Task ProcessSteps()
{
    status = "Step 1...";
    await Step1Async();
    status = "Step 2...";
    StateHasChanged(); // intermediate update
    await Step2Async();
}
  1. 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).

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download author-component.zip