Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

83 Commits

Repository files navigation

TaskManagerAPI

CI.NETC#SQL ServerDocker

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.

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.


Table of Contents


Architecture

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

flowchart TD
Client([Client])
Gateway[Gateway.Api<br/>YARP Reverse Proxy]
Identity[IdentityService.Api<br/>Register / Login<br/>JWT Issuer]
Tasks[TasksService.Api<br/>Todo CRUD<br/>JWT-protected]
DB[(SQL Server<br/>Dockerized)]
Client --> Gateway
Gateway --> Identity
Gateway --> Tasks
Identity --> DB
Tasks --> DB
Loading

Each service is layered:

  • Domain: entities, interfaces, no external dependencies
  • Application: CQRS handlers (MediatR), DTOs, validators, mapping profiles
  • Infrastructure: EF Core DbContext, repositories, JWT provider, external service implementations
  • Api: controllers, middleware, DI composition root

Tech Stack

ConcernChoice
Runtime.NET 10 / C# 13
API GatewayYARP (Yet Another Reverse Proxy)
AuthASP.NET Core Identity + JWT Bearer
CQRS / MediatorMediatR
ValidationFluentValidation
ORMEntity Framework Core 10
DatabaseSQL Server 2022 (containerized)
MappingAutoMapper
LoggingSerilog
API DocsScalar (OpenAPI)
Unit TestsxUnit, Moq, FluentAssertions
Integration TestsWebApplicationFactory, Testcontainers.MsSql
Build AutomationNuke Build
CIGitHub Actions
ContainerizationDocker + Docker Compose

Key Features

  • Clean Architecture: enforced per service. Domain has zero dependencies; Api depends on Application; Infrastructure plugs in at composition root.
  • CQRS with MediatR: every use case is a Command or Query with its own handler. No fat controllers.
  • JWT authentication: issued by IdentityService, consumed by TasksService through shared validation parameters.
  • FluentValidation pipeline: requests validated before reaching handlers via a MediatR behavior.
  • Global exception handling: using IExceptionHandler returning RFC 7807 ProblemDetails.
  • Pagination: on list endpoints (pageNumber, pageSize) with total count metadata.
  • Result<T> pattern: handlers return explicit success/failure instead of throwing for control flow.
  • Secrets management: User Secrets for local dev, .env + docker-compose.override.yml for Docker; nothing sensitive committed.
  • Fail-fast configuration: services validate connection strings and JWT keys at startup, not per-request.
  • Integration tests with real SQL Server: via Testcontainers not InMemory fakes. Tests spin up a disposable SQL Server container per test class.

Project Structure

TaskManagerAPI/
├── Gateway.Api/ # YARP reverse proxy
├── Common/ # Shared Result<T> type
│
├── IdentityService.Api/ # Auth endpoints, DI composition root
├── IdentityService.Application/ # CQRS handlers, DTOs, validators
├── IdentityService.Domain/ # User entity, service interfaces
├── IdentityService.Infrastructure/ # EF Core, ASP.NET Identity, JWT provider
├── IdentityService.UnitTests/
├── IdentityService.IntegrationTests/
│
├── TasksService.Api/ # Todo endpoints (JWT-protected)
├── TasksService.Application/ # CQRS handlers, pagination, validators
├── TasksService.Domain/ # TodoItem entity, repository interface
├── TasksService.Infrastructure/ # EF Core DbContext, repository impl
├── TasksService.UnitTests/
├── TasksService.IntegrationTests/
│
├── build/ # Nuke build automation
├── docker-compose.yml
├── docker-compose.override.example.yml
└── .env.example

API Reference

Full interactive docs are available at /scalar/v1 on each service when running locally.

IdentityService

MethodEndpointAuthDescription
POST/api/auth/registerCreate a new user account
POST/api/auth/loginAuthenticate and receive a JWT

TasksService

MethodEndpointAuthDescription
GET/api/todos?pageNumber=1&pageSize=10JWTList current user's todos (paginated)
GET/api/todos/{id}JWTGet a single todo
POST/api/todosJWTCreate a todo
PUT/api/todos/{id}JWTUpdate a todo
DELETE/api/todos/{id}JWTDelete a todo

All TasksService routes are also reachable through the gateway at http://localhost:5001/tasks/....


API Docs Preview

Interactive Scalar documentation is generated from the OpenAPI spec at /scalar/v1 for each service.

Tasks API docs
TasksService Scalar UI

Auth API docs
IdentityService Scalar UI


Installation

Prerequisites

Clone

git clone https://github.com/LolghElmo/TaskManagerAPI.git
cd TaskManagerAPI

Configure secrets

Copy the example env file and fill in a password + JWT key:

cp .env.example .env
cp docker-compose.override.example.yml docker-compose.override.yml

Edit .env:

SA_PASSWORD=Your_Strong_Password_123!JWT_SECRET_KEY=a-long-random-string-at-least-32-characters

For local (non-Docker) runs, use User Secrets instead:

cd IdentityService.Api
dotnet user-secrets set"ConnectionStrings:DefaultConnection""Server=localhost;Database=IdentityDb;Trusted_Connection=True;TrustServerCertificate=True"
dotnet user-secrets set"JwtSettings:SecretKey""a-long-random-string-at-least-32-characters"

Repeat for TasksService.Api with its own connection string (e.g. Database=TasksDb) and the sameJwtSettings:SecretKey so it can validate tokens issued by IdentityService.


Running with Docker

Spin up everything (SQL Server + all three services) with one command:

docker compose up --build

Services will be available at:

  • Gateway: http://localhost:5001
  • IdentityService: http://localhost:5002
  • TasksService: http://localhost:5003

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.

Tear down:

docker compose down # keeps the volume
docker compose down -v # wipes the database volume

Testing

dotnet test

The suite currently runs 20 tests across 4 projects (15 unit, 5 integration), split into two styles:

  • Unit tests (*.UnitTests): handlers tested in isolation with Moq'd repositories, mappers, and loggers. Fast, no I/O.
  • Integration tests (*.IntegrationTests): spin up a real SQL Server container via Testcontainers, 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.

Integration tests set environment variables (ConnectionStrings__DefaultConnection, JwtSettings__SecretKey) in IAsyncLifetime.InitializeAsyncbefore the host is built — this is the key to making WebApplicationFactory work with Testcontainers.


CI/CD

Continuous integration runs on every push via GitHub Actions. The workflow is generated from a typed Nuke build using the [GitHubActions] attribute with no hand-written YAML.

Run the same pipeline locally:

./build.sh # or build.cmd / build.ps1

Targets: Clean -> Restore -> Compile -> Test.


What I Learned

A few things this project taught me that I wouldn't have picked up from a tutorial:

  • Testcontainers + WebApplicationFactory: environment variables for connection strings have to be set in IAsyncLifetime.InitializeAsyncbefore the host builds. ConfigureAppConfiguration is too late under minimal hosting.
  • Result 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.
  • YARP route transforms: without them, the gateway leaks the internal service topology straight to the client. Route prefixes matter.
  • 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.
  • 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.
  • Docker healthchecks + depends_on: service_healthy: without them, services race SQL Server on first boot and crashloop until it's ready.

Roadmap

Things I'd add next:

  • Refresh tokens + token revocation
  • Redis-backed distributed cache for GET /api/todos
  • Health check endpoints (/health, /health/ready)
  • Structured request logging with correlation IDs across services
  • RabbitMQ/MassTransit for an async "task assigned" event
  • OpenTelemetry + Jaeger/Seq for distributed tracing
  • Deploy to Azure Container Apps

Acknowledgements

This project was built as my first full-stack backend. I leaned on these resources to learn the patterns:

About

Microservices task management API built with .NET 10, Clean Architecture, CQRS, JWT auth, YARP gateway, SQL Server, Testcontainers integration tests, Nuke + GitHub Actions CI.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages