Claude Cursor GitHub Copilot Skill

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),

LLM Mart · 0 points · 17 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download dotnet-skills-plugins_dotnet-blazor_skills_author-component-98f8485.zip · 6 KB
Part of dotnet/skills — 119 skills

Install

skills CLI npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-blazor/skills/author-component
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart
Git 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 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 (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.

No comments yet.

Reviews (0)

No reviews yet.

Related