dotnet/blazor-project-starter/SKILL.md
Scaffold a production-ready Blazor .NET 9 application with Server, WebAssembly, and Auto render modes, component architecture, SignalR real-time, dependency injection, JS interop, authentication state, and CSS isolation.
npx skillsauth add achreftlili/deep-dev-skills blazor-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold a production-ready Blazor .NET 9 application with Server, WebAssembly, and Auto render modes, component architecture, SignalR real-time, dependency injection, JS interop, authentication state, and CSS isolation.
# Blazor Web App (Server + WebAssembly hybrid — recommended for .NET 9)
dotnet new blazor -n <ProjectName> --interactivity Auto --all-interactive
cd <ProjectName>
# Add packages
dotnet add <ProjectName> package Microsoft.AspNetCore.Components.QuickGrid
dotnet add <ProjectName> package Microsoft.EntityFrameworkCore.Design
dotnet add <ProjectName> package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add <ProjectName>.Client package Microsoft.AspNetCore.Components.WebAssembly.Authentication
# Create test project
dotnet new xunit -n <ProjectName>.Tests
dotnet add <ProjectName>.Tests reference <ProjectName>
dotnet add <ProjectName>.Tests package bunit
dotnet add <ProjectName>.Tests package FluentAssertions
# Solution
dotnet new sln
dotnet sln add <ProjectName>
dotnet sln add <ProjectName>.Client
dotnet sln add <ProjectName>.Tests
<ProjectName>/ # Server project (host)
Program.cs # Server entry — services, middleware, render modes
Components/
App.razor # Root component — <head>, <body>, render mode config
Routes.razor # Router component
Layout/
MainLayout.razor # Shared layout with nav
MainLayout.razor.css # CSS isolation for layout
NavMenu.razor
Pages/
Home.razor # @page "/" — routable component
Users/
UserList.razor # @page "/users"
UserDetail.razor # @page "/users/{Id:guid}"
UserForm.razor # Shared create/edit form
Shared/
LoadingSpinner.razor # Reusable non-routable components
ConfirmDialog.razor
ErrorBoundaryWrapper.razor
Services/
IUserService.cs # Shared interface
UserService.cs # Server-side implementation (EF Core)
Data/
AppDbContext.cs
wwwroot/
css/
js/
interop.js # JS interop functions
<ProjectName>.Client/ # WebAssembly project
Program.cs # WASM entry — HttpClient, services
Pages/
Counter.razor # WASM-only interactive pages
Services/
UserServiceHttp.cs # Client-side implementation (HttpClient)
@rendermode InteractiveServer (SignalR), @rendermode InteractiveWebAssembly (WASM), @rendermode InteractiveAuto (server first, then WASM after download)@rendermode only to components that need interactivity.razor files — mix C# and HTML with Razor syntax@code {} block for component logic, or code-behind files (Component.razor.cs) for complex componentsComponent.razor.css scopes styles to that component automatically@inject in Razor or constructor injection in code-behind[Parameter] for parent-to-child, [CascadingParameter] for deep prop drilling, EventCallback<T> for child-to-parentNavigationManager for programmatic navigation, not <a> with JavaScriptProgram.csvar builder = WebApplication.CreateBuilder(args);
// Blazor services
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
// Database
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// Services — server-side implementations
builder.Services.AddScoped<IUserService, UserService>();
// Authentication
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthentication().AddCookie();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(<ProjectName>.Client._Imports).Assembly);
app.Run();
Components/App.razor<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="<ProjectName>.styles.css" />
<HeadOutlet @rendermode="InteractiveAuto" />
</head>
<body>
<Routes @rendermode="InteractiveAuto" />
<script src="_framework/blazor.web.js"></script>
</body>
</html>
Components/Pages/Users/UserList.razor@page "/users"
@inject IUserService UserService
@inject NavigationManager Navigation
@rendermode InteractiveServer
<PageTitle>Users</PageTitle>
<h1>Users</h1>
@if (_users is null)
{
<LoadingSpinner />
}
else if (!_users.Any())
{
<p>No users found.</p>
}
else
{
<QuickGrid Items="@_users.AsQueryable()" Pagination="@_pagination">
<PropertyColumn Property="@(u => u.Name)" Sortable="true" />
<PropertyColumn Property="@(u => u.Email)" Sortable="true" />
<PropertyColumn Property="@(u => u.CreatedAt)" Title="Created" Format="yyyy-MM-dd" />
<TemplateColumn Title="Actions">
<button class="btn btn-sm btn-primary" @onclick="() => ViewUser(context.Id)">
View
</button>
</TemplateColumn>
</QuickGrid>
<Paginator State="@_pagination" />
}
<button class="btn btn-success mt-3" @onclick="CreateNew">New User</button>
@code {
private List<UserDto>? _users;
private PaginationState _pagination = new() { ItemsPerPage = 20 };
protected override async Task OnInitializedAsync()
{
_users = await UserService.GetAllAsync();
}
private void ViewUser(Guid id) => Navigation.NavigateTo($"/users/{id}");
private void CreateNew() => Navigation.NavigateTo("/users/new");
}
Components/Pages/Users/UserDetail.razor@page "/users/{Id:guid}"
@inject IUserService UserService
@rendermode InteractiveServer
<PageTitle>User Detail</PageTitle>
@if (_user is null)
{
<LoadingSpinner />
}
else
{
<h1>@_user.Name</h1>
<dl>
<dt>Email</dt>
<dd>@_user.Email</dd>
<dt>Created</dt>
<dd>@_user.CreatedAt.ToString("yyyy-MM-dd HH:mm")</dd>
</dl>
<UserForm User="@_user" OnSave="HandleSave" />
}
@code {
[Parameter]
public Guid Id { get; set; }
private UserDto? _user;
protected override async Task OnParameterSetAsync()
{
_user = await UserService.GetByIdAsync(Id);
}
private async Task HandleSave(UserDto updated)
{
await UserService.UpdateAsync(Id, updated);
_user = updated;
}
}
Components/Pages/Users/UserForm.razor<EditForm Model="@User" OnValidSubmit="HandleSubmit" FormName="user-form">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<InputText id="name" class="form-control" @bind-Value="User.Name" />
<ValidationMessage For="@(() => User.Name)" />
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<InputText id="email" class="form-control" @bind-Value="User.Email" />
<ValidationMessage For="@(() => User.Email)" />
</div>
<button type="submit" class="btn btn-primary" disabled="@_submitting">
@(_submitting ? "Saving..." : "Save")
</button>
</EditForm>
@code {
[Parameter, EditorRequired]
public UserDto User { get; set; } = default!;
[Parameter]
public EventCallback<UserDto> OnSave { get; set; }
private bool _submitting;
private async Task HandleSubmit()
{
_submitting = true;
await OnSave.InvokeAsync(User);
_submitting = false;
}
}
Components/Pages/Users/UserList.razor.cssh1 {
color: #1a1a2e;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 0.5rem;
}
/* ::deep targets child components within this scope */
::deep .btn-sm {
font-size: 0.75rem;
}
Services/IUserService.cspublic interface IUserService
{
Task<List<UserDto>> GetAllAsync();
Task<UserDto?> GetByIdAsync(Guid id);
Task<UserDto> CreateAsync(UserDto user);
Task UpdateAsync(Guid id, UserDto user);
Task DeleteAsync(Guid id);
}
public class UserDto
{
public Guid Id { get; set; }
[Required, StringLength(100)]
public string Name { get; set; } = "";
[Required, EmailAddress]
public string Email { get; set; } = "";
public DateTime CreatedAt { get; set; }
}
Services/UserService.cspublic class UserService(AppDbContext db) : IUserService
{
public async Task<List<UserDto>> GetAllAsync()
{
return await db.Users
.OrderByDescending(u => u.CreatedAt)
.Select(u => new UserDto
{
Id = u.Id,
Name = u.Name,
Email = u.Email!,
CreatedAt = u.CreatedAt
})
.ToListAsync();
}
public async Task<UserDto?> GetByIdAsync(Guid id)
{
var user = await db.Users.FindAsync(id);
if (user is null) return null;
return new UserDto { Id = user.Id, Name = user.Name, Email = user.Email!, CreatedAt = user.CreatedAt };
}
public async Task<UserDto> CreateAsync(UserDto dto)
{
var user = new User { Name = dto.Name, Email = dto.Email, UserName = dto.Email };
db.Users.Add(user);
await db.SaveChangesAsync();
dto.Id = user.Id;
dto.CreatedAt = user.CreatedAt;
return dto;
}
public async Task UpdateAsync(Guid id, UserDto dto)
{
var user = await db.Users.FindAsync(id)
?? throw new InvalidOperationException($"User {id} not found");
user.Name = dto.Name;
user.Email = dto.Email;
await db.SaveChangesAsync();
}
public async Task DeleteAsync(Guid id)
{
var user = await db.Users.FindAsync(id)
?? throw new InvalidOperationException($"User {id} not found");
db.Users.Remove(user);
await db.SaveChangesAsync();
}
}
Client/Services/UserServiceHttp.csusing System.Net.Http.Json;
public class UserServiceHttp(HttpClient http) : IUserService
{
public async Task<List<UserDto>> GetAllAsync()
=> await http.GetFromJsonAsync<List<UserDto>>("api/users") ?? [];
public async Task<UserDto?> GetByIdAsync(Guid id)
=> await http.GetFromJsonAsync<UserDto>($"api/users/{id}");
public async Task<UserDto> CreateAsync(UserDto user)
{
var response = await http.PostAsJsonAsync("api/users", user);
response.EnsureSuccessStatusCode();
return (await response.Content.ReadFromJsonAsync<UserDto>())!;
}
public async Task UpdateAsync(Guid id, UserDto user)
{
var response = await http.PutAsJsonAsync($"api/users/{id}", user);
response.EnsureSuccessStatusCode();
}
public async Task DeleteAsync(Guid id)
{
var response = await http.DeleteAsync($"api/users/{id}");
response.EnsureSuccessStatusCode();
}
}
wwwroot/js/interop.jswindow.appInterop = {
showAlert: function (message) {
alert(message);
},
getWindowDimensions: function () {
return { width: window.innerWidth, height: window.innerHeight };
},
downloadFile: function (fileName, contentType, base64) {
const link = document.createElement('a');
link.download = fileName;
link.href = `data:${contentType};base64,${base64}`;
link.click();
}
};
@inject IJSRuntime JS
<button @onclick="Download">Download Report</button>
@code {
private async Task Download()
{
var dimensions = await JS.InvokeAsync<WindowDimensions>("appInterop.getWindowDimensions");
// Use dimensions...
// Trigger file download
var base64Content = Convert.ToBase64String(reportBytes);
await JS.InvokeVoidAsync("appInterop.downloadFile", "report.csv", "text/csv", base64Content);
}
private record WindowDimensions(int Width, int Height);
}
Components/Shared/AuthView.razor<AuthorizeView>
<Authorized>
<p>Welcome, @context.User.Identity?.Name</p>
<button class="btn btn-outline-danger" @onclick="Logout">Logout</button>
</Authorized>
<NotAuthorized>
<a href="/login" class="btn btn-primary">Login</a>
</NotAuthorized>
</AuthorizeView>
@code {
[CascadingParameter]
private Task<AuthenticationState>? AuthState { get; set; }
private async Task Logout()
{
// Handle logout
}
}
Components/Shared/ErrorBoundaryWrapper.razor<ErrorBoundary @ref="_errorBoundary">
<ChildContent>
@ChildContent
</ChildContent>
<ErrorContent Context="ex">
<div class="alert alert-danger">
<h4>Something went wrong</h4>
<p>@ex.Message</p>
<button class="btn btn-sm btn-outline-danger" @onclick="Recover">Try Again</button>
</div>
</ErrorContent>
</ErrorBoundary>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; } = default!;
private ErrorBoundary? _errorBoundary;
private void Recover() => _errorBoundary?.Recover();
}
Tests/Components/UserListTests.csusing Bunit;
using FluentAssertions;
using Moq;
public class UserListTests : TestContext
{
[Fact]
public void ShowsUsersWhenLoaded()
{
var mockService = new Mock<IUserService>();
mockService.Setup(s => s.GetAllAsync())
.ReturnsAsync([
new UserDto { Id = Guid.NewGuid(), Name = "Alice", Email = "[email protected]" }
]);
Services.AddSingleton(mockService.Object);
var cut = RenderComponent<UserList>();
cut.WaitForState(() => cut.FindAll("tr").Count > 0);
cut.Markup.Should().Contain("Alice");
}
}
# Development (hot reload)
dotnet watch run --project <ProjectName>
# Run
dotnet run --project <ProjectName>
# Build
dotnet build
# Publish
dotnet publish -c Release -o ./publish
# Tests
dotnet test
# EF Core migrations
dotnet ef migrations add InitialCreate --project <ProjectName>
dotnet ef database update --project <ProjectName>
# Add a Razor component (manual — no scaffold command)
# Create ComponentName.razor in the target folder
InteractiveServer for database-heavy pages (no API needed, direct EF access). Use InteractiveWebAssembly for offline-capable or low-latency UI. Use InteractiveAuto to start on server then transition to WASM automatically.HubConnection and connect to a custom Hub class.CascadingValue or a state container pattern.IJSRuntime.InvokeAsync. Call C# from JS by passing DotNetObjectReference and using [JSInvokable] methods.EditForm with DataAnnotationsValidator for validation. Use InputText, InputNumber, InputSelect for two-way binding. OnValidSubmit fires only when validation passes.Component.razor.css files. Use ::deep to target child component elements. Global styles go in wwwroot/css/app.css.mcr.microsoft.com/dotnet/sdk:9.0 for build, mcr.microsoft.com/dotnet/aspnet:9.0 for runtime.OnInitializedAsync for side effects that should not run twice — use OnAfterRenderAsync(firstRender) instead.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.