with one click
dotnet-blazor
// Build and review Blazor applications across server, WebAssembly, web app, and hybrid scenarios with correct component design, state flow, rendering, and hosting choices.
// Build and review Blazor applications across server, WebAssembly, web app, and hybrid scenarios with correct component design, state flow, rendering, and hosting choices.
Use a repo-root `.editorconfig` to configure free .NET analyzer and style rules. Use when a .NET repo needs rule severity, code-style options, section layout, or analyzer ownership made explicit. Nested `.editorconfig` files are allowed when they serve a clear subtree-specific purpose.
Build, debug, modernize, or review ASP.NET Core applications with correct hosting, middleware, security, configuration, logging, and deployment patterns on current .NET.
Use Biome in .NET repositories that ship Node-based frontend assets and want a fast combined formatter-linter-import organizer for JavaScript, TypeScript, CSS, JSON, GraphQL, or HTML. Use when a repo prefers a modern all-in-one CLI over a larger ESLint plus Prettier style stack.
Use ESLint in .NET repositories that ship JavaScript, TypeScript, React, or other Node-based frontend assets. Use when a repo needs a configurable CLI lint gate for frontend correctness, import hygiene, unsafe patterns, or framework-specific rules.
Use HTMLHint in .NET repositories that ship static HTML output or standalone frontend templates. Use when a repo needs a focused CLI lint gate for DOM structure, invalid attributes, and basic HTML correctness checks on static pages.
Use ManagedCode.MarkItDown when a .NET application needs deterministic document-to-Markdown conversion for ingestion, indexing, summarization, or content-processing workflows.
| name | dotnet-blazor |
| description | Build and review Blazor applications across server, WebAssembly, web app, and hybrid scenarios with correct component design, state flow, rendering, and hosting choices. |
| compatibility | Requires Blazor project (.NET 6+, preferably .NET 8+ for unified model). |
| Mode | Where It Runs | Best For |
|---|---|---|
Static | Server (no interactivity) | SEO pages, marketing content |
InteractiveServer | Server via SignalR | Real-time apps, thin clients |
InteractiveWebAssembly | Browser via WASM | Offline-capable, client-heavy |
InteractiveAuto | Server first, then WASM | Best of both worlds |
@* Per-component *@
@rendermode InteractiveServer
@* Or in App.razor for global *@
<Routes @rendermode="InteractiveAuto" />
First Request:
Browser → Server (Interactive Server) → Fast response
Subsequent Requests:
Browser → WASM (downloaded in background) → No server needed
Choose render mode based on requirements:
Design components for reusability:
Handle state correctly:
[PersistentState]Validate in both environments (for Auto mode)
@* Counter.razor *@
<button @onclick="IncrementCount">
Clicked @count times
</button>
@code {
private int count = 0;
[Parameter]
public int InitialCount { get; set; } = 0;
protected override void OnInitialized()
{
count = InitialCount;
}
private void IncrementCount() => count++;
}
@* Parent.razor *@
<ChildComponent Value="@value" ValueChanged="@OnValueChanged" />
@* ChildComponent.razor *@
@code {
[Parameter] public string Value { get; set; } = "";
[Parameter] public EventCallback<string> ValueChanged { get; set; }
private async Task UpdateValue(string newValue)
{
await ValueChanged.InvokeAsync(newValue);
}
}
@* Prevents double-fetch during prerender + hydration *@
@code {
[PersistentState]
public List<Product> Products { get; set; } = [];
protected override async Task OnInitializedAsync()
{
// Only fetches once, persisted across prerender
Products ??= await Http.GetFromJsonAsync<List<Product>>("api/products");
}
}
// Shared interface
public interface IProductService
{
Task<List<Product>> GetProductsAsync();
}
// Server implementation (direct DB access)
public class ServerProductService : IProductService
{
private readonly AppDbContext _db;
public async Task<List<Product>> GetProductsAsync()
=> await _db.Products.ToListAsync();
}
// Client implementation (HTTP call)
public class ClientProductService : IProductService
{
private readonly HttpClient _http;
public async Task<List<Product>> GetProductsAsync()
=> await _http.GetFromJsonAsync<List<Product>>("api/products");
}
// Registration
// Server: builder.Services.AddScoped<IProductService, ServerProductService>();
// Client: builder.Services.AddScoped<IProductService, ClientProductService>();
| Anti-Pattern | Why It's Bad | Better Approach |
|---|---|---|
| Large components | Hard to maintain, slow renders | Split into smaller components |
| Direct DB access in WASM | No DB in browser | Use HTTP API |
Ignoring ShouldRender | Unnecessary re-renders | Override when needed |
| Sync JS interop in Server | Blocks SignalR circuit | Use IJSRuntime async |
| No error boundaries | One error crashes app | Use <ErrorBoundary> |
| Forgetting prerender state | Double API calls | Use [PersistentState] |
Virtualize large lists:
<Virtualize Items="@products" Context="product">
<ProductCard Product="@product" />
</Virtualize>
Use @key for list diffing:
@foreach (var item in items)
{
<ItemComponent @key="item.Id" Item="@item" />
}
Debounce rapid events:
private Timer? _debounceTimer;
private void OnInput(ChangeEventArgs e)
{
_debounceTimer?.Dispose();
_debounceTimer = new Timer(_ => InvokeAsync(DoSearch), null, 300, Timeout.Infinite);
}
Lazy load assemblies (WASM):
var assemblies = await LazyAssemblyLoader
.LoadAssembliesAsync(["MyHeavyFeature.wasm"]);
@inject IJSRuntime JS
await JS.InvokeVoidAsync("alert", "Hello from Blazor!");
var result = await JS.InvokeAsync<string>("prompt", "Enter name:");
[JSInvokable]
public static string GetMessage() => "Hello from C#!";
DotNet.invokeMethodAsync('MyAssembly', 'GetMessage')
.then(result => console.log(result));