|
1 | | -# TaskManagerAPI |
| 1 | +# TaskManagerAPI |
| 2 | + |
| 3 | +[](https://github.com/LolghElmo/TaskManagerAPI/actions/workflows/CI.yml) |
| 4 | + |
| 5 | + |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +A microservices-based task management API built with **.NET 10**, designed as an exercise in applying real-world backend patterns: Clean Architecture, CQRS, JWT authentication, a reverse-proxy API gateway, containerized SQL Server, and full testing including integration tests with real databases via Testcontainers. |
| 10 | + |
| 11 | +> This is my first end-to-end backend project built to learn, not copied from a template. Everything here (architecture decisions, test setup, CI, Docker wiring) was figured out step by step. The goal was to ship something that mirrors how a production service is actually structured, not a toy CRUD app. |
| 12 | +
|
| 13 | +--- |
| 14 | + |
| 15 | +## Table of Contents |
| 16 | + |
| 17 | +- [Architecture](#architecture) |
| 18 | +- [Tech Stack](#tech-stack) |
| 19 | +- [Key Features](#key-features) |
| 20 | +- [Project Structure](#project-structure) |
| 21 | +- [API Reference](#api-reference) |
| 22 | +- [Installation](#installation) |
| 23 | +- [Running with Docker](#running-with-docker) |
| 24 | +- [Testing](#testing) |
| 25 | +- [CI/CD](#cicd) |
| 26 | +- [What I Learned](#what-i-learned) |
| 27 | +- [Roadmap](#roadmap) |
| 28 | +- [Acknowledgements](#acknowledgements) |
| 29 | + |
| 30 | +--- |
| 31 | + |
| 32 | +## Architecture |
| 33 | + |
| 34 | +Three independently deployable services sit behind a YARP reverse-proxy gateway. Each service owns its own database and follows Clean Architecture (Domain -> Application -> Infrastructure -> Api). |
| 35 | + |
| 36 | +```mermaid |
| 37 | +flowchart TD |
| 38 | + Client([Client]) |
| 39 | + Gateway[Gateway.Api<br/>YARP Reverse Proxy] |
| 40 | + Identity[IdentityService.Api<br/>Register / Login<br/>JWT Issuer] |
| 41 | + Tasks[TasksService.Api<br/>Todo CRUD<br/>JWT-protected] |
| 42 | + DB[(SQL Server<br/>Dockerized)] |
| 43 | +
|
| 44 | + Client --> Gateway |
| 45 | + Gateway --> Identity |
| 46 | + Gateway --> Tasks |
| 47 | + Identity --> DB |
| 48 | + Tasks --> DB |
| 49 | +``` |
| 50 | + |
| 51 | +Each service is layered: |
| 52 | + |
| 53 | +- **Domain**: entities, interfaces, no external dependencies |
| 54 | +- **Application**: CQRS handlers (MediatR), DTOs, validators, mapping profiles |
| 55 | +- **Infrastructure**: EF Core `DbContext`, repositories, JWT provider, external service implementations |
| 56 | +- **Api**: controllers, middleware, DI composition root |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +## Tech Stack |
| 61 | + |
| 62 | +| Concern | Choice | |
| 63 | +|---|---| |
| 64 | +| Runtime | .NET 10 / C# 13 | |
| 65 | +| API Gateway | YARP (Yet Another Reverse Proxy) | |
| 66 | +| Auth | ASP.NET Core Identity + JWT Bearer | |
| 67 | +| CQRS / Mediator | MediatR | |
| 68 | +| Validation | FluentValidation | |
| 69 | +| ORM | Entity Framework Core 10 | |
| 70 | +| Database | SQL Server 2022 (containerized) | |
| 71 | +| Mapping | AutoMapper | |
| 72 | +| Logging | Serilog | |
| 73 | +| API Docs | Scalar (OpenAPI) | |
| 74 | +| Unit Tests | xUnit, Moq, FluentAssertions | |
| 75 | +| Integration Tests | `WebApplicationFactory`, Testcontainers.MsSql | |
| 76 | +| Build Automation | Nuke Build | |
| 77 | +| CI | GitHub Actions | |
| 78 | +| Containerization | Docker + Docker Compose | |
| 79 | + |
| 80 | +--- |
| 81 | + |
| 82 | +## Key Features |
| 83 | + |
| 84 | +- **Clean Architecture**: enforced per service. Domain has zero dependencies; Api depends on Application; Infrastructure plugs in at composition root. |
| 85 | +- **CQRS with MediatR**: every use case is a `Command` or `Query` with its own handler. No fat controllers. |
| 86 | +- **JWT authentication**: issued by `IdentityService`, consumed by `TasksService` through shared validation parameters. |
| 87 | +- **FluentValidation pipeline**: requests validated before reaching handlers via a MediatR behavior. |
| 88 | +- **Global exception handling**: using `IExceptionHandler` returning RFC 7807 `ProblemDetails`. |
| 89 | +- **Pagination**: on list endpoints (`pageNumber`, `pageSize`) with total count metadata. |
| 90 | +- **Result<T> pattern**: handlers return explicit success/failure instead of throwing for control flow. |
| 91 | +- **Secrets management**: User Secrets for local dev, `.env` + `docker-compose.override.yml` for Docker; nothing sensitive committed. |
| 92 | +- **Fail-fast configuration**: services validate connection strings and JWT keys at startup, not per-request. |
| 93 | +- **Integration tests with real SQL Server**: via Testcontainers not InMemory fakes. Tests spin up a disposable SQL Server container per test class. |
| 94 | + |
| 95 | +--- |
| 96 | + |
| 97 | +## Project Structure |
| 98 | + |
| 99 | +``` |
| 100 | +TaskManagerAPI/ |
| 101 | +├── Gateway.Api/ # YARP reverse proxy |
| 102 | +├── Common/ # Shared Result<T> type |
| 103 | +│ |
| 104 | +├── IdentityService.Api/ # Auth endpoints, DI composition root |
| 105 | +├── IdentityService.Application/ # CQRS handlers, DTOs, validators |
| 106 | +├── IdentityService.Domain/ # User entity, service interfaces |
| 107 | +├── IdentityService.Infrastructure/ # EF Core, ASP.NET Identity, JWT provider |
| 108 | +├── IdentityService.UnitTests/ |
| 109 | +├── IdentityService.IntegrationTests/ |
| 110 | +│ |
| 111 | +├── TasksService.Api/ # Todo endpoints (JWT-protected) |
| 112 | +├── TasksService.Application/ # CQRS handlers, pagination, validators |
| 113 | +├── TasksService.Domain/ # TodoItem entity, repository interface |
| 114 | +├── TasksService.Infrastructure/ # EF Core DbContext, repository impl |
| 115 | +├── TasksService.UnitTests/ |
| 116 | +├── TasksService.IntegrationTests/ |
| 117 | +│ |
| 118 | +├── build/ # Nuke build automation |
| 119 | +├── docker-compose.yml |
| 120 | +├── docker-compose.override.example.yml |
| 121 | +└── .env.example |
| 122 | +``` |
| 123 | + |
| 124 | +--- |
| 125 | + |
| 126 | +## API Reference |
| 127 | + |
| 128 | +Full interactive docs are available at `/scalar/v1` on each service when running locally. |
| 129 | + |
| 130 | +### IdentityService |
| 131 | + |
| 132 | +| Method | Endpoint | Auth | Description | |
| 133 | +|---|---|---|---| |
| 134 | +| POST | `/api/auth/register` | — | Create a new user account | |
| 135 | +| POST | `/api/auth/login` | — | Authenticate and receive a JWT | |
| 136 | + |
| 137 | +### TasksService |
| 138 | + |
| 139 | +| Method | Endpoint | Auth | Description | |
| 140 | +|---|---|---|---| |
| 141 | +| GET | `/api/todos?pageNumber=1&pageSize=10` | JWT | List current user's todos (paginated) | |
| 142 | +| GET | `/api/todos/{id}` | JWT | Get a single todo | |
| 143 | +| POST | `/api/todos` | JWT | Create a todo | |
| 144 | +| PUT | `/api/todos/{id}` | JWT | Update a todo | |
| 145 | +| DELETE | `/api/todos/{id}` | JWT | Delete a todo | |
| 146 | + |
| 147 | +All `TasksService` routes are also reachable through the gateway at `http://localhost:5001/tasks/...`. |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +## Installation |
| 152 | + |
| 153 | +### Prerequisites |
| 154 | + |
| 155 | +- [.NET 10 SDK](https://dotnet.microsoft.com/download) |
| 156 | +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) |
| 157 | +- (Optional) Visual Studio 2022+ or JetBrains Rider |
| 158 | + |
| 159 | +### Clone |
| 160 | + |
| 161 | +```bash |
| 162 | +git clone https://github.com/LolghElmo/TaskManagerAPI.git |
| 163 | +cd TaskManagerAPI |
| 164 | +``` |
| 165 | + |
| 166 | +### Configure secrets |
| 167 | + |
| 168 | +Copy the example env file and fill in a password + JWT key: |
| 169 | + |
| 170 | +```bash |
| 171 | +cp .env.example .env |
| 172 | +cp docker-compose.override.example.yml docker-compose.override.yml |
| 173 | +``` |
| 174 | + |
| 175 | +Edit `.env`: |
| 176 | + |
| 177 | +```env |
| 178 | +SA_PASSWORD=Your_Strong_Password_123! |
| 179 | +JWT_SECRET_KEY=a-long-random-string-at-least-32-characters |
| 180 | +``` |
| 181 | + |
| 182 | +For local (non-Docker) runs, use User Secrets instead: |
| 183 | + |
| 184 | +```bash |
| 185 | +cd IdentityService.Api |
| 186 | +dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;Database=IdentityDb;Trusted_Connection=True;TrustServerCertificate=True" |
| 187 | +dotnet user-secrets set "JwtSettings:SecretKey" "a-long-random-string-at-least-32-characters" |
| 188 | +``` |
| 189 | + |
| 190 | +Repeat for `TasksService.Api` with its own connection string (e.g. `Database=TasksDb`) and the **same** `JwtSettings:SecretKey` so it can validate tokens issued by `IdentityService`. |
| 191 | + |
| 192 | +--- |
| 193 | + |
| 194 | +## Running with Docker |
| 195 | + |
| 196 | +Spin up everything (SQL Server + all three services) with one command: |
| 197 | + |
| 198 | +```bash |
| 199 | +docker compose up --build |
| 200 | +``` |
| 201 | + |
| 202 | +Services will be available at: |
| 203 | + |
| 204 | +- Gateway: `http://localhost:5001` |
| 205 | +- IdentityService: `http://localhost:5002` |
| 206 | +- TasksService: `http://localhost:5003` |
| 207 | + |
| 208 | +The SQL Server container runs a healthcheck, and both API services wait for `service_healthy` before starting — no more race-condition crashes on first boot. |
| 209 | + |
| 210 | +Tear down: |
| 211 | + |
| 212 | +```bash |
| 213 | +docker compose down # keeps the volume |
| 214 | +docker compose down -v # wipes the database volume |
| 215 | +``` |
| 216 | + |
| 217 | +--- |
| 218 | + |
| 219 | +## Testing |
| 220 | + |
| 221 | +```bash |
| 222 | +dotnet test |
| 223 | +``` |
| 224 | + |
| 225 | +The suite currently runs **20 tests** across 4 projects (15 unit, 5 integration), split into two styles: |
| 226 | + |
| 227 | +- **Unit tests** (`*.UnitTests`): handlers tested in isolation with Moq'd repositories, mappers, and loggers. Fast, no I/O. |
| 228 | +- **Integration tests** (`*.IntegrationTests`): spin up a real SQL Server container via [Testcontainers](https://dotnet.testcontainers.org/), boot the actual API through `WebApplicationFactory<Program>`, and hit endpoints over HTTP. Catches the bugs unit tests can't: EF migrations, JSON serialization, auth middleware, DI wiring. |
| 229 | + |
| 230 | +Integration tests set environment variables (`ConnectionStrings__DefaultConnection`, `JwtSettings__SecretKey`) in `IAsyncLifetime.InitializeAsync` **before** the host is built — this is the key to making `WebApplicationFactory` work with Testcontainers. |
| 231 | + |
| 232 | +--- |
| 233 | + |
| 234 | +## CI/CD |
| 235 | + |
| 236 | +Continuous integration runs on every push via GitHub Actions. The workflow is generated from a typed [Nuke](https://nuke.build/) build using the `[GitHubActions]` attribute with no hand-written YAML. |
| 237 | + |
| 238 | +Run the same pipeline locally: |
| 239 | + |
| 240 | +```bash |
| 241 | +./build.sh # or build.cmd / build.ps1 |
| 242 | +``` |
| 243 | + |
| 244 | +Targets: `Clean -> Restore -> Compile -> Test`. |
| 245 | + |
| 246 | +--- |
| 247 | + |
| 248 | +## What I Learned |
| 249 | + |
| 250 | +A few things this project taught me that I wouldn't have picked up from a tutorial: |
| 251 | + |
| 252 | +- **Testcontainers + `WebApplicationFactory`**: environment variables for connection strings have to be set in `IAsyncLifetime.InitializeAsync` *before* the host builds. `ConfigureAppConfiguration` is too late under minimal hosting. |
| 253 | +- **Result<T> over exceptions**: using an explicit result type for expected failures (login failed, todo not found) made handlers easier to test and removed most of the try/catch noise from controllers. |
| 254 | +- **YARP route transforms**: without them, the gateway leaks the internal service topology straight to the client. Route prefixes matter. |
| 255 | +- **Fail-fast config validation**: checking connection strings and JWT keys with `string.IsNullOrWhiteSpace` at startup surfaces config problems immediately instead of as cryptic 500s on the first request. |
| 256 | +- **Nuke generating GitHub Actions**: the `[GitHubActions]` attribute replaces hand-written YAML entirely the same Test target runs locally and on CI, so "works on my machine" is no longer a thing. |
| 257 | +- **Docker healthchecks + `depends_on: service_healthy`**: without them, services race SQL Server on first boot and crashloop until it's ready. |
| 258 | + |
| 259 | +--- |
| 260 | + |
| 261 | +## Roadmap |
| 262 | + |
| 263 | +Things I'd add next: |
| 264 | + |
| 265 | +- Refresh tokens + token revocation |
| 266 | +- Redis-backed distributed cache for `GET /api/todos` |
| 267 | +- Health check endpoints (`/health`, `/health/ready`) |
| 268 | +- Structured request logging with correlation IDs across services |
| 269 | +- RabbitMQ/MassTransit for an async "task assigned" event |
| 270 | +- OpenTelemetry + Jaeger/Seq for distributed tracing |
| 271 | +- Deploy to Azure Container Apps |
| 272 | + |
| 273 | +--- |
| 274 | + |
| 275 | +## Acknowledgements |
| 276 | + |
| 277 | +This project was built as my first full-stack backend. I leaned on these resources to learn the patterns: |
| 278 | + |
| 279 | +- [Clean Architecture with ASP.NET Core 10](https://www.youtube.com/watch?v=rjefnUC9Z90) |
| 280 | +- [How to Implement the CQRS Pattern in Clean Architecture (from scratch)](https://www.youtube.com/watch?v=85YbMEb1qkQ) |
| 281 | +- [Intro to MediatR — Implementing CQRS and Mediator Patterns](https://www.youtube.com/watch?v=yozD5Tnd8nw) |
| 282 | +- [ASP.NET Core Integration Testing Tutorial](https://www.youtube.com/watch?v=RXSPCIrrjHc) |
| 283 | +- [The Best Way To Use Docker For Integration Testing In .NET](https://www.youtube.com/watch?v=tj5ZCtvgXKY) |
| 284 | +- Microsoft Docs — ASP.NET Core, EF Core, Identity |
| 285 | +- [Testcontainers for .NET](https://dotnet.testcontainers.org/) documentation |
| 286 | +- YARP official samples |
| 287 | + |
0 commit comments