CtrlK
BlogDocsLog inGet started
Tessl Logo

blazor-project-starter

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

Quality

57%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./dotnet/blazor-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

Blazor Project Starter

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.

Prerequisites

  • .NET 9 SDK
  • Node.js (optional — for npm-based CSS tooling)
  • Visual Studio 2022+ or VS Code with C# Dev Kit

Scaffold Command

# 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

Project Structure

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

Key Conventions

  • .NET 9 Blazor unifies Server and WebAssembly into a single project with render modes per-component
  • Render modes: @rendermode InteractiveServer (SignalR), @rendermode InteractiveWebAssembly (WASM), @rendermode InteractiveAuto (server first, then WASM after download)
  • Static SSR by default — add @rendermode only to components that need interactivity
  • Components are .razor files — mix C# and HTML with Razor syntax
  • Use @code {} block for component logic, or code-behind files (Component.razor.cs) for complex components
  • CSS isolation: Component.razor.css scopes styles to that component automatically
  • Dependency injection via @inject in Razor or constructor injection in code-behind
  • Parameters: [Parameter] for parent-to-child, [CascadingParameter] for deep prop drilling, EventCallback<T> for child-to-parent
  • Use NavigationManager for programmatic navigation, not <a> with JavaScript

Essential Patterns

Server Entry Point — Program.cs

var 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();

Root Component — 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>

Routable Page Component — 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");
}

Detail Page with Route Parameter — 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;
    }
}

Reusable Form Component — 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;
    }
}

CSS Isolation — Components/Pages/Users/UserList.razor.css

h1 {
    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;
}

Service Interface — Services/IUserService.cs

public 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; }
}

Server-Side Service — Services/UserService.cs

public 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-Side HTTP Service — Client/Services/UserServiceHttp.cs

using 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();
    }
}

JavaScript Interop — wwwroot/js/interop.js

window.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();
    }
};

Using JS Interop in a Component

@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);
}

Authentication State — 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
    }
}

Error Boundary — 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();
}

bUnit Test — Tests/Components/UserListTests.cs

using 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");
    }
}

Common Commands

# 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

Integration Notes

  • Render Modes: Use 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.
  • SignalR: Server render mode uses SignalR under the hood. For custom real-time features, inject HubConnection and connect to a custom Hub class.
  • State Management: Use cascading parameters for auth state. For complex state, create a scoped service registered in DI. For cross-component state, use CascadingValue or a state container pattern.
  • JS Interop: Call JS from C# with IJSRuntime.InvokeAsync. Call C# from JS by passing DotNetObjectReference and using [JSInvokable] methods.
  • Forms: EditForm with DataAnnotationsValidator for validation. Use InputText, InputNumber, InputSelect for two-way binding. OnValidSubmit fires only when validation passes.
  • CSS: Scoped via Component.razor.css files. Use ::deep to target child component elements. Global styles go in wwwroot/css/app.css.
  • Docker: Same as ASP.NET Core — mcr.microsoft.com/dotnet/sdk:9.0 for build, mcr.microsoft.com/dotnet/aspnet:9.0 for runtime.
  • Prerendering: Static SSR prerenders on the server for SEO, then hydrates with the chosen render mode. Avoid using OnInitializedAsync for side effects that should not run twice — use OnAfterRenderAsync(firstRender) instead.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

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.