diff --git a/catalog/Frameworks/Official-DotNet-ASPNet/skills/convert-blazor-server-to-webapp/SKILL.md b/catalog/Frameworks/Official-DotNet-ASPNet/skills/convert-blazor-server-to-webapp/SKILL.md new file mode 100644 index 0000000..819ed43 --- /dev/null +++ b/catalog/Frameworks/Official-DotNet-ASPNet/skills/convert-blazor-server-to-webapp/SKILL.md @@ -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 + net8.0 + ``` +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 `` 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 ``, 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 + + + + + + + +

Sorry, there's nothing at this address.

+
+
+
+``` + +If the app uses `` instead of ``, 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 `` with ``. + +4. **Replace HeadOutlet Component Tag Helper** — replace: + ```html + + ``` + with: + ```razor + + ``` + +5. **Replace App Component Tag Helper with Routes** — replace: + ```html + + ``` + with: + ```razor + + ``` + +6. **Replace Environment Tag Helpers** — replace: + ```html + + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + ``` + with: + ```razor + @if (Env.IsDevelopment()) + { + + An unhandled exception has occurred. See browser dev tools for details. + + } + else + { + + An error has occurred. This app may no longer respond until reloaded. + + } + ``` + +7. **Update the Blazor script** — replace: + ```html + + ``` + with: + ```html + + ``` + +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() + .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 `` to wrap the router: + +1. Remove the `` 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` 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 `` tag referenced a `_Host` assembly name; ensure it matches the project's actual assembly name: ``. +- 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 `` configuration +- [ ] `Program.cs` uses `AddRazorComponents().AddInteractiveServerComponents()` +- [ ] `Program.cs` uses `MapRazorComponents().AddInteractiveServerRenderMode()` +- [ ] `app.UseAntiforgery()` is present in the middleware pipeline +- [ ] If the app used ``, 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 `` 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 `` 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 diff --git a/catalog/Frameworks/Official-DotNet-ASPNet/skills/convert-blazor-server-to-webapp/manifest.json b/catalog/Frameworks/Official-DotNet-ASPNet/skills/convert-blazor-server-to-webapp/manifest.json new file mode 100644 index 0000000..65c9d92 --- /dev/null +++ b/catalog/Frameworks/Official-DotNet-ASPNet/skills/convert-blazor-server-to-webapp/manifest.json @@ -0,0 +1,5 @@ +{ + "version": "0.1.0", + "category": "Web", + "compatibility": "Requires an ASP.NET Core project or solution." +} diff --git a/catalog/Platform/Official-DotNet-Blazor/manifest.json b/catalog/Platform/Official-DotNet-Blazor/manifest.json new file mode 100644 index 0000000..6dd55b8 --- /dev/null +++ b/catalog/Platform/Official-DotNet-Blazor/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "dotnet-blazor", + "title": "Official .NET skills: dotnet-blazor", + "description": "Skills for Blazor development: component authoring, interactivity, and web application patterns.", + "links": { + "repository": "https://github.com/dotnet/skills", + "docs": "https://github.com/dotnet/skills/tree/main/plugins/dotnet-blazor" + } +} diff --git a/catalog/Platform/Official-DotNet-Blazor/skills/author-component/SKILL.md b/catalog/Platform/Official-DotNet-Blazor/skills/author-component/SKILL.md new file mode 100644 index 0000000..58343e5 --- /dev/null +++ b/catalog/Platform/Official-DotNet-Blazor/skills/author-component/SKILL.md @@ -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` (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` (not `IEnumerable`) for collection parameters. + +## RenderFragment & Generics + +```csharp +[Parameter] public RenderFragment? ChildContent { get; set; } +[Parameter] public RenderFragment? 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`): 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` +- `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` diff --git a/catalog/Platform/Official-DotNet-Blazor/skills/author-component/manifest.json b/catalog/Platform/Official-DotNet-Blazor/skills/author-component/manifest.json new file mode 100644 index 0000000..6197565 --- /dev/null +++ b/catalog/Platform/Official-DotNet-Blazor/skills/author-component/manifest.json @@ -0,0 +1,5 @@ +{ + "version": "0.1.0", + "category": "Core", + "compatibility": "Requires a .NET repository or solution." +} diff --git a/catalog/Platform/Official-DotNet-Blazor/skills/author-component/references/async-programming-rules.md b/catalog/Platform/Official-DotNet-Blazor/skills/author-component/references/async-programming-rules.md new file mode 100644 index 0000000..a332be9 --- /dev/null +++ b/catalog/Platform/Official-DotNet-Blazor/skills/author-component/references/async-programming-rules.md @@ -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>("api/items"); +} + +// DON'T — fire-and-forget hides exceptions +private void LoadData() +{ + _ = Http.GetFromJsonAsync>("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`, `BlockingCollection`, 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>("api/items").Result; +} + +// Correct — use async all the way through +private async Task Load() +{ + var data = await Http.GetFromJsonAsync>("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>("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` — use plain collections + +Because the synchronization context guarantees single-threaded access within a circuit, regular `Dictionary`, `List`, and `Queue` are safe. Concurrent collections add overhead with no benefit: + +```csharp +// Wrong — unnecessary overhead, hides the threading model +private readonly ConcurrentDictionary cache = new(); + +// Correct — the sync context already prevents concurrent access +private readonly Dictionary 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>("api/items") + .ContinueWith(t => + { + items = t.Result; + StateHasChanged(); // InvalidOperationException! + }); +} + +// Correct — straightforward async/await +private async Task Start() +{ + items = await Http.GetFromJsonAsync>("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 + +

@status

+ +@code { + private string status = "Loading..."; + private CancellationTokenSource cts = new(); + + protected override async Task OnInitializedAsync() + { + try + { + var data = await Http.GetFromJsonAsync>( + "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; + } +} +``` diff --git a/catalog/Platform/Official-DotNet-Blazor/skills/author-component/references/breaking-down-components.md b/catalog/Platform/Official-DotNet-Blazor/skills/author-component/references/breaking-down-components.md new file mode 100644 index 0000000..9196425 --- /dev/null +++ b/catalog/Platform/Official-DotNet-Blazor/skills/author-component/references/breaking-down-components.md @@ -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 + +
+

@Title

+ +
+@code { + [Parameter, EditorRequired] public string Title { get; set; } = ""; + [Parameter] public EventCallback OnPin { get; set; } +} +``` + +```razor + +
+

@Description

+ +
+@code { + [Parameter, EditorRequired] public string Description { get; set; } = ""; + [Parameter] public EventCallback OnExpand { get; set; } +} +``` + +```razor + +
+ + +
+``` + +## List-Item Extraction + +Extract complex item templates into their own component. Use `@key` for efficient diffing. + +```razor + +
  • + + @Task.Title + +
  • +@code { + [Parameter, EditorRequired] public TaskModel Task { get; set; } = default!; + [Parameter] public EventCallback OnToggle { get; set; } + [Parameter] public EventCallback OnDelete { get; set; } +} +``` + +```razor + +
      + @foreach (var task in Tasks) + { + + } +
    +``` + +## Cascading Context + +Avoid parameter drilling through intermediate components. Cascade a context object or cascade the parent itself. + +```razor + + + + +
    @ActiveTab?.ChildContent
    + +@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 + +@implements ITab +
  • + @Title +
  • +@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 { ... });` diff --git a/catalog/Platform/Official-DotNet-Blazor/skills/author-component/references/component-disposal.md b/catalog/Platform/Official-DotNet-Blazor/skills/author-component/references/component-disposal.md new file mode 100644 index 0000000..58c82a0 --- /dev/null +++ b/catalog/Platform/Official-DotNet-Blazor/skills/author-component/references/component-disposal.md @@ -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`). 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("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. diff --git a/catalog/Platform/Official-DotNet-Blazor/skills/collect-user-input/SKILL.md b/catalog/Platform/Official-DotNet-Blazor/skills/collect-user-input/SKILL.md new file mode 100644 index 0000000..be59710 --- /dev/null +++ b/catalog/Platform/Official-DotNet-Blazor/skills/collect-user-input/SKILL.md @@ -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 + + + + + + + + + +@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 + +``` + +## 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 `` | +| `InputTextArea` | `string` | Renders `