A production-ready, multi-tenant todo application built with ASP.NET Core 10 and Blazor
Features Β· Architecture Β· Getting Started Β· API Reference Β· Configuration Β· Testing Β· Contributing
- π Task management β Create, edit, complete, and organise todos with priorities (Low / Medium / High), categories, and due dates
- π Real-time search β Server-side search across titles, notes, and categories with debounced input
- π Pagination & sorting β Configurable page size, sort by title / priority / due date / last updated
- ποΈ Trash & restore β Soft-delete with a dedicated trash bin; restore or permanently delete anytime
- β° Overdue alerts β Dismissible banner when tasks have slipped past their due date
- π Dark mode β System-aware dark/light toggle that persists across sessions
- π± Responsive layout β Sidebar collapses on mobile; full-width single-column view
- π’ Multi-tenancy β Full three-tier tenant isolation (Administrator β TenantAdmin β User)
- π₯ User management β Create users, assign roles, lock/unlock accounts, manage across tenants
- π Role-based access β Granular policy enforcement at every endpoint
- π Audit log β Every create, update, complete, delete, and restore action recorded with a full diff
- β€οΈ Health checks β
/health/live(liveness) and/health/ready(readiness with DB probe) - π Distributed tracing β OpenTelemetry with ASP.NET Core + EF Core instrumentation
- π Metrics β Runtime metrics (GC, heap, thread pool) via OpenTelemetry
- π Structured logging β Serilog with per-request enrichment:
UserId,TenantId,TraceId,SpanId - π Refresh tokens β 30-day sliding refresh tokens with rotation on use
- π¦ Rate limiting β Configurable fixed-window limits per endpoint group
TodoApp follows Onion Architecture (also known as Clean Architecture), with strict dependency flow: outer layers depend on inner layers, never the reverse.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Presentation β
β β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββββββ β
β β TodoApp.Web β β TodoApp.Api β β
β β Blazor Server UI βββββββΆβ ASP.NET Core Minimal APIβ β
β ββββββββββββββββββββββββ JWT ββββββββββββββ¬ββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββ
β Infrastructure β
β TodoApp.Infrastructure β
β EF Core Β· SQLite Β· Identity Β· JWT Β· Migrations β
β DatabaseSeeder β
ββββββββββββββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββ
β Application β
β TodoApp.Application β
β TodoService Β· AuthService Β· DTOs Β· Interfaces β
ββββββββββββββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββ
β Domain β
β TodoApp.Domain β
β TodoItem Β· AppUser Β· Tenant Β· Enums Β· Interfaces β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Todo 2/
βββ π TodoApp.Domain/ # Core entities and interfaces (no dependencies)
β βββ π Entities/ # TodoItem, AppUser, Tenant, AuditLog
β βββ π Enums/ # Priority
β βββ π Interfaces/ # ITodoRepository, etc.
β
βββ π TodoApp.Application/ # Use-case logic (depends on Domain only)
β βββ π Services/ # TodoService, AuthService
β βββ π DTOs/ # Request / response models
β βββ DependencyInjection.cs # AddApplication() extension
β
βββ π TodoApp.Infrastructure/ # Data access + external concerns
β βββ π Data/ # TodoDbContext, AppUser (Identity)
β βββ π Repositories/ # TodoRepository (EF Core)
β βββ π Migrations/ # EF Core migration history
β βββ π Seeding/ # DatabaseSeeder (roles + super-admin)
β βββ DependencyInjection.cs # AddInfrastructure() extension
β
βββ π TodoApp.Api/ # Thin Minimal API layer
β βββ π Endpoints/ # Auth, Todos, Admin, TenantAdmin
β βββ π Middleware/ # Global exception handler
β βββ Program.cs # AddInfrastructure() + AddApplication() + pipeline
β
βββ π TodoApp.Web/ # Blazor Server frontend
β βββ π Components/
β β βββ π Pages/ # Landing, Login, Home, Trash, Admin, TenantAdmin
β β βββ π Layout/ # MainLayout, NavMenu
β βββ π Services/ # AuthService, TodoApiService
β βββ π Models/ # Request/response DTOs
β βββ π wwwroot/ # CSS, PWA manifest
β
βββ π TodoApp.Api.Tests/ # Integration tests (WebApplicationFactory)
β βββ AuthEndpointsTests.cs
β βββ TodoEndpointsTests.cs
β βββ AdminEndpointsTests.cs
β βββ TenantAdminEndpointsTests.cs
β βββ HealthTests.cs
β
βββ π TodoApp.Application.Tests/ # Pure unit tests (NSubstitute, no HTTP)
βββ TodoServiceTests.cs
βββ AuthServiceTests.cs
Administrator (no TenantId)
βββ manages all tenants and all users platform-wide
TenantAdmin (scoped TenantId)
βββ manages users within their own tenant only
User (scoped TenantId)
βββ manages their own todos only
All users are created by an administrator β there is no public self-registration. Tenant isolation is enforced at the database query level via EF Core global query filters on every request.
- .NET 10 SDK
- A terminal (PowerShell, bash, zsh)
git clone https://github.com/cvizzini/claw-todo.git
cd claw-todocd TodoApp.Api
dotnet runThe API starts on http://localhost:5227. On first run it will:
- Create the SQLite database (
todos.db) - Run all EF Core migrations automatically
- Seed roles and the default super-admin account
Default super-admin credentials:
| Field | Value |
|---|---|
admin@todoapp.local |
|
| Password | Admin1234! |
β οΈ Change these inappsettings.jsonbefore deploying to production.
# In a separate terminal
cd TodoApp.Web
dotnet runOpen http://localhost:5001 in your browser and sign in with the super-admin credentials above.
# Integration tests
cd TodoApp.Api.Tests
dotnet test
# Unit tests
cd TodoApp.Application.Tests
dotnet testExpected output: 107 tests, 0 failures.
With the API running in Development mode, visit:
http://localhost:5227/scalar
Interactive Scalar UI with every endpoint documented and try-it-out support.
Migrations live in TodoApp.Infrastructure. Always specify both projects:
dotnet ef migrations add <MigrationName> \
--project TodoApp.Infrastructure \
--startup-project TodoApp.Api
dotnet ef database update \
--project TodoApp.Infrastructure \
--startup-project TodoApp.ApiAll endpoints are versioned under /api/v1/.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/login |
β | Sign in, returns JWT + refresh token |
POST |
/refresh |
β | Exchange refresh token for new token pair |
GET |
/me |
β | Get current user profile |
| Method | Endpoint | Description |
|---|---|---|
GET |
/ |
List todos (paginated, searchable, filterable, sortable) |
POST |
/ |
Create a todo |
GET |
/{id} |
Get a single todo |
PUT |
/{id} |
Update a todo |
PATCH |
/{id}/toggle |
Toggle completion status |
DELETE |
/{id} |
Soft-delete (moves to trash) |
GET |
/trash |
List soft-deleted todos |
POST |
/{id}/restore |
Restore from trash |
DELETE |
/{id}/permanent |
Permanently delete from trash |
DELETE |
/trash |
Empty trash |
DELETE |
/completed |
Clear all completed todos |
GET |
/stats |
Counts: total, active, completed, overdue |
GET |
/categories |
Distinct categories used by this user |
GET |
/{id}/audit |
Audit history for a specific todo |
Pagination & filtering:
GET /api/v1/todos?page=1&pageSize=20&search=groceries&category=Personal&completed=false&sortBy=dueDate&sortDesc=false
| Method | Endpoint | Description |
|---|---|---|
GET |
/tenants |
List all tenants |
POST |
/tenants |
Create a tenant |
GET |
/tenants/{id} |
Get tenant by ID |
PUT |
/tenants/{id} |
Update tenant name / active status |
DELETE |
/tenants/{id} |
Delete tenant (must have no users) |
GET |
/users |
List all users (optionally filter by ?tenantId=) |
GET |
/users/{id} |
Get user by ID |
POST |
/users |
Create user with role + tenant assignment |
PUT |
/users/{id} |
Update display name, role, tenant |
DELETE |
/users/{id} |
Hard delete user and all their data |
PATCH |
/users/{id}/lock |
Lock account |
PATCH |
/users/{id}/unlock |
Unlock account |
Same user management endpoints as Admin, automatically scoped to the caller's tenant. Cannot create or manage Administrators.
| Endpoint | Description |
|---|---|
GET /health/live |
Liveness probe β { "status": "alive" } |
GET /health/ready |
Readiness probe β includes database connectivity check |
GET /health |
Full health report with all checks and durations |
All configuration lives in appsettings.json. Key sections:
Any appsettings.json key can be overridden via environment variable using __ as the separator:
Jwt__Key=my-super-secret-key
Seed__AdminPassword=SecurePassword123!
OpenTelemetry__Endpoint=http://localhost:4317Before going to production:
- Replace the JWT
Keywith a cryptographically random secret (β₯ 32 characters) - Change the default seed admin email and password
- Switch
ConnectionStrings:DefaultConnectionfrom SQLite to PostgreSQL - Set
AllowedHoststo your specific domain - Configure HTTPS and set
ASPNETCORE_URLS - Point
OpenTelemetry:Endpointat your observability backend
The test suite uses xUnit + FluentAssertions with two complementary layers:
Uses WebApplicationFactory with a shared in-memory SQLite connection β no mocks, real middleware pipeline. Every test gets a fully seeded environment (admin, tenant, tenant-admin, and regular user).
cd TodoApp.Api.Tests
dotnet test --verbosity normal| Suite | Tests | Covers |
|---|---|---|
AuthEndpointsTests |
8 | Login, refresh, me, lockout |
TodoEndpointsTests |
26 | Full CRUD, soft delete, audit, search, pagination |
AdminEndpointsTests |
22 | Tenant CRUD, user management, cross-tenant isolation |
TenantAdminEndpointsTests |
9 | Scoped admin operations, role enforcement |
HealthTests |
3 | Liveness, readiness, full report |
| Subtotal | 74 |
Pure unit tests of the Application layer using NSubstitute for mocking. No HTTP, no database, no EF Core. Tests service logic in isolation.
cd TodoApp.Application.Tests
dotnet test --verbosity normal| Suite | Tests | Covers |
|---|---|---|
TodoServiceTests |
22 | Create, update, delete, toggle, trash, restore, stats |
AuthServiceTests |
11 | Login, refresh, JWT validation, role checks |
| Subtotal | 33 |
Total: 107 tests β Failed: 0, Passed: 107
- PostgreSQL support β production-grade database with connection pooling
- Docker + Docker Compose β multi-stage build with Postgres + Seq for local dev
- GitHub Actions CI β build β test β publish on PR
- Password reset β email-based forgot-password flow
- Problem Details β RFC 9457
application/problem+jsonon all error responses - Email verification β wire up
EmailConfirmedflag - PWA / offline support β service worker + offline cache
Contributions are welcome! Here's how to get started:
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes β keep all 107 tests green
- Add tests for any new behaviour
- Push and open a Pull Request
Please keep PRs focused β one feature or fix per PR makes review much easier.
This project is licensed under the MIT License β see the LICENSE file for details.
Built with β€οΈ using ASP.NET Core 10 Β· Blazor Β· Entity Framework Core Β· OpenTelemetry
{ "ConnectionStrings": { // SQLite by default β swap for PostgreSQL in production "DefaultConnection": "Data Source=todos.db" }, "Jwt": { "Key": "CHANGE-THIS-TO-A-LONG-RANDOM-SECRET", // min 32 chars "Issuer": "TodoApp.Api", "Audience": "TodoApp.Web", "ExpiryHours": "8" }, "Seed": { // Super-admin created on first run only "AdminEmail": "admin@yourcompany.com", "AdminPassword": "ChangeMe1234!", "AdminDisplayName": "Super Admin" }, "RateLimiting": { "AuthLimit": 10, // requests per minute on /auth "ApiLimit": 100 // requests per minute on /api }, "OpenTelemetry": { "ServiceName": "TodoApp.Api", "Endpoint": "" // OTLP endpoint (Seq, Jaeger, etc.) β empty = console in Development } }