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.
65
57%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./dotnet/blazor-project-starter/SKILL.mdScaffold 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 = "alice@test.com" }
]);
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 folderInteractiveServer 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.181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.