TodosApi is a collaborative task and note management backend built with ASP.NET Core 9 minimal APIs. It provides JWT-secured REST endpoints for managing users, todos, notes, and friendships together with real-time notifications delivered over WebSockets. The solution is split into API, Core domain, Infrastructure, and automated test projects to keep concerns isolated and maintainable.
- Token-based authentication using JWT with refresh-free access tokens.
- Todo and note CRUD with fine-grained access control (owner, collaborators, friends).
- Friendship workflows (requests, accept/decline, blocking) that control sharing shortcuts.
- WebSocket hub (
/ws) that streams binary update signals to connected clients and handles ping/pong heartbeats. - Centralised exception handling and validation helpers that translate domain errors into consistent API responses.
- SQLite persistence by default with Entity Framework Core migrations; in-memory database automatically used during tests.
- End-to-end, integration, and unit testing coverage using xUnit, WebApplicationFactory, Moq, and EF Core InMemory.
- Dockerfile and
docker-compose.yamlfor containerised deployment behind Nginx, plus helper scripts for deployment and smoke tests.
| Path | Description |
|---|---|
Api/ |
ASP.NET Core minimal API surface (endpoints, DTOs, middleware, WebSocket hub, services). |
Core/ |
Domain models, enums, interfaces, and settings shared across the solution. |
Infrastructure/ |
Entity Framework Core context, migrations, repository implementations, and data access. |
TodosApi.Tests/ |
xUnit integration and unit tests targeting the API and domain logic. |
docker-compose.yaml, DockerFile |
Container build and runtime definitions (API + Nginx reverse proxy). |
deploy.sh, end-to-end-test.sh |
Convenience scripts for VPS deployment and full happy-path smoke testing. |
- .NET SDK 9.0
- ASP.NET Core minimal APIs & middleware
- Entity Framework Core 9 (SQLite + InMemory providers)
- JWT (Microsoft.IdentityModel.Tokens)
- WebSockets
- Swashbuckle / OpenAPI for interactive docs
- xUnit + WebApplicationFactory + Moq for automated tests
- .NET 9 SDK
- SQLite 3 (CLI optional but useful for inspecting
todos.db) - (Optional) Docker Engine + Docker Compose v2 for container workflows
Validate the SDK globally:
dotnet --list-sdksEnsure 9.0.x is listed.
- Restore dependencies:
dotnet restore Api/Api.csproj
- Update configuration as needed. The development settings live in
Api/appsettings.Development.json. At minimum replace the placeholderJwtSettings:SecretKeywith a 32+ character string. - Apply database migrations (creates
Api/todos.db):dotnet ef database update \ --project Infrastructure/Infrastructure.csproj \ --startup-project Api/Api.csproj
The command requires the
dotnet-efCLI tool; install it once withdotnet tool install --global dotnet-ef. - Run the API:
The app listens on the default Kestrel ports (an HTTPS + HTTP pair printed in the console). Swagger UI is available at
dotnet run --project Api/Api.csproj
/swaggerwhenASPNETCORE_ENVIRONMENT=Development. - Connect a WebSocket client at
ws://localhost:<http-port>/ws?token=<jwt>to receive realtime update notifications.
| Key | Purpose |
|---|---|
ConnectionStrings:Sqlite |
Path to the SQLite database file. The production profile points to /app/todos.db. |
JwtSettings:SecretKey |
HMAC secret used to sign tokens. Replace the placeholder before deploying. |
JwtSettings:Issuer, Audience, ExpirationMinutes |
JWT validation parameters for API and clients. |
UseTesting |
When true (or when ASPNETCORE_ENVIRONMENT=Testing) the API switches EF Core to the in-memory provider, used by automated tests. |
TestDatabaseName |
Optional override for the in-memory database name during tests. |
All keys can be supplied via environment variables using the double-underscore (__) notation (e.g., JwtSettings__SecretKey).
Run the full test suite (unit + integration):
dotnet test- Integration tests spin up an in-memory version of the API using
WebApplicationFactoryand setUseTesting=trueto avoid touching your local SQLite file. - Unit tests cover domain rules and repository behaviours with in-memory dependencies.
For a scripted end-to-end sanity check against a running local instance (expected at http://localhost:5126), run:
./end-to-end-test.shThe script walks through registration, login, todo/note CRUD, sharing, friendships, and authentication edge cases. Update API_BASE inside the script if your server listens on a different host/port.
Build and start the stack (API + Nginx reverse proxy):
docker compose up --build -dThe compose file exposes the API through Nginx on ports 80 and 443, and maps Api/appsettings.Production.json into the container. Update that file with production secrets before building. The SQLite database file persists via the bind-mounted Api/ folder.
The helper script ./deploy.sh wraps Docker installation checks, secret generation, rebuild, and container startup - handy for provisioning a fresh Linux server.
- WebSocket endpoint:
ws(s)://<host>/ws - Authentication: supply the JWT either in the
Authorizationheader during the HTTP handshake or as thetokenquery string (the server explicitly looks at the query parameter for WebSocket clients). - Messages: binary payloads containing an
UpdateTypebyte (seeCore/DomainObjects/UpdateType.cs). Clients should translate the enum to decide which data needs refreshing; the API doesn't push full resource payloads over the socket. - Keep-alive: send
"ping"text frames periodically; the hub replies with"pong"and tracks connection health for cleanup.
| Area | Endpoints |
|---|---|
| Auth & Users | POST /api/users/register, POST /api/users/login, GET /api/users/{id}, PUT /api/users/{id}, POST /api/users/{id}/change-password, GET /api/users/search/{term} |
| Todos | GET /api/todos/user, POST /api/todos, PUT /api/todos/{todoId}, DELETE /api/todos/{todoId}, POST /api/todos/{todoId}/complete, POST /api/todos/{todoId}/access, POST /api/todos/{todoId}/share-with-friends |
| Notes | GET /api/notes/user, GET /api/notes/{noteId}, POST /api/notes, PUT /api/notes/{noteId}, DELETE /api/notes/{noteId}, POST /api/notes/{noteId}/access, POST /api/notes/{noteId}/share-with-friends |
| Friendships | POST /api/friendships/send, POST /api/friendships/accept, POST /api/friendships/decline, POST /api/friendships/block/{targetUserId}, GET /api/friendships/pending, GET /api/friendships/friends, GET /api/friendships/check/{userId1}/{userId2}, DELETE /api/friendships/{friendId} |
| WebSockets | GET /ws?token=<jwt> (upgrade to WebSocket) |
All authenticated endpoints expect a Bearer <token> header. Validation errors (400), authorization failures (401/403), and domain errors share a consistent ApiResponse payload shape defined under Api/Models.
Happy hacking!