Production-ready .NET 10 service template with Clean Architecture, Docker, Helm, and GitHub Actions CI/CD.
- Architecture
- Prerequisites
- Quick Start
- Project Structure
- Development Workflow
- Testing
- Docker
- Kubernetes / Helm
- CI/CD
- Renaming the Template
- Configuration Reference
This template follows Clean Architecture (also known as Onion Architecture):
┌────────────────────────────────────────┐
│ Api (Entry Point) │ Minimal APIs, middleware, DI wiring
├────────────────────────────────────────┤
│ Application Layer │ CQRS (ISender/IRequestHandler), validators, DTOs
├────────────────────────────────────────┤
│ Domain Layer │ Entities, value objects, domain errors
├────────────────────────────────────────┤
│ Infrastructure Layer │ EF Core, PostgreSQL, repositories
└────────────────────────────────────────┘
Key design decisions:
- Custom CQRS — commands and queries dispatched via
ISender.SendAsync()/IRequestHandler.HandleAsync(), with pipeline behaviors for logging and validation (MediatR is commercial — not used) - Result pattern — no exceptions for expected failure paths; errors flow as values
- Testcontainers — integration and acceptance tests spin up real PostgreSQL containers
- OpenTelemetry — traces, metrics, and logs exported via OTLP
- Serilog — structured logging with Seq support for local dev
| Tool | Minimum Version | Install |
|---|---|---|
| .NET SDK | 10.0.x | dotnet.microsoft.com |
| Docker Desktop | 4.x | docker.com |
make |
any | winget install GnuWin32.Make (Windows) |
git |
2.x | git-scm.com |
Optional (recommended):
- VS Code with the C# Dev Kit extension
- Dev Containers extension for a fully containerised dev environment
# 1. Clone and enter the project
git clone https://github.com/your-org/service-template.git && cd service-template
# 2. One-command setup: installs tools, restores packages, starts infra, runs migrations
make setup
# 3. Start the API with hot reload
make runThe API is now available at http://localhost:5000.
- Scalar API docs:
http://localhost:5000/scalar - Health check:
http://localhost:5000/health - Seq logs:
http://localhost:5341
- Open the repository in VS Code
- When prompted, click "Reopen in Container"
- Wait for the container to build (~2 min on first run)
- Run
make setupin the integrated terminal
.
├── .devcontainer/ # VS Code Dev Container configuration
├── .github/
│ ├── workflows/
│ │ ├── ci.yml # Build, test, Docker build on push/PR
│ │ ├── release.yml # Push Docker image + Helm chart on tag
│ │ └── pr.yml # PR title validation, format check
│ ├── CODEOWNERS
│ └── pull_request_template.md
├── deploy/
│ ├── helm/chart/ # Helm chart (deployment, HPA, PDB, ingress...)
│ └── otel/ # OpenTelemetry Collector config
├── docs/adr/ # Architecture Decision Records
├── src/
│ ├── Api/ # ASP.NET Core Minimal API, Program.cs
│ ├── Application/ # Custom CQRS (ISender/IRequestHandler), validators, DTOs
│ ├── Domain/ # Entities, domain errors, Result<T>
│ └── Infrastructure/ # EF Core, PostgreSQL, repositories
├── tests/
│ ├── UnitTests/ # Fast, isolated, no I/O
│ ├── IntegrationTests/ # Real DB via Testcontainers + Respawn
│ └── AcceptanceTests/ # BDD-style HTTP API tests
├── docker-compose.yml # Full local stack
├── docker-compose.override.yml # Local overrides (auto-applied by Docker Compose)
├── Dockerfile # Multi-stage production build
├── Directory.Build.props # Shared MSBuild settings for all projects
├── Directory.Packages.props # Central NuGet package version management
├── global.json # Pins .NET SDK version
├── Makefile # Developer task runner
└── ServiceTemplate.sln
make help # Show all available commands
make build # Build the solution
make run # Run API with hot reload
make fmt # Auto-format code
make lint # Verify formatting (CI-safe)
make clean # Remove bin/obj/TestResults
make outdated # List outdated NuGet packages# Apply pending migrations
make migrate
# Create a new migration
make migration NAME=AddUserTable
# Roll back last migration
make migration-rollbackmake infra-up # Start Postgres + Seq + OTEL Collector
make infra-down # Stop them
make infra-reset # Full reset — WARNING: deletes all data| Test Suite | Purpose | Command |
|---|---|---|
| Unit | Business logic in isolation (NSubstitute mocks) | make test |
| Integration | Full app + real PostgreSQL (Testcontainers) | make test-integration |
| Acceptance | BDD-style HTTP API tests | make test-acceptance |
make test-all # Run every test suite
make coverage # Unit tests + open HTML coverage reportCoverage is automatically uploaded to Codecov in CI.
# Build the image
make docker-build
# Start the complete stack
make docker-up
# Tail logs
make docker-logs
# Stop everything
make docker-downThe multi-stage Dockerfile produces a minimal, non-root runtime image.
# Lint the chart
make helm-lint
# Render templates (dry-run)
make helm-template
# Package
make helm-package
# Deploy to staging
helm upgrade --install my-service deploy \
-f deploy/values.yaml \
-f values.staging.yaml \
--set image.tag=1.2.3 \
--namespace my-namespace --create-namespaceProduction values are not stored in this repo.
Production deployments are managed via a dedicated GitOps repository that references the Helm chart and container image released from here. Supply your production values there.
The chart includes:
- Deployment with rolling updates and
maxUnavailable: 0 - Horizontal Pod Autoscaler (CPU + memory)
- PodDisruptionBudget (
minAvailable: 1) - Non-root pod security context with
readOnlyRootFilesystem - Topology spread constraints for zone-level HA
- Liveness / readiness probes
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml |
Push to main/develop, PR |
Build → Unit Tests → Integration Tests → Acceptance Tests → Docker build |
release.yml |
Push tag v*.*.* |
All tests → Push Docker image to GHCR → Package & push Helm chart → GitHub Release |
pr.yml |
PR opened/updated | Validate PR title (Conventional Commits), check code formatting |
git tag v1.2.3
git push origin v1.2.3This triggers the release pipeline which will:
- Run all tests
- Build and push
ghcr.io/your-org/service-template:1.2.3 - Push the Helm chart to
ghcr.io/your-org/helm-charts - Create a GitHub Release with auto-generated notes
To use this as a starting point for a new service named PaymentService:
# 1. Clone
git clone https://github.com/your-org/service-template.git payment-service
cd payment-service
# 2. Bulk rename (Linux/macOS)
find . -not -path './.git/*' \
\( -name "*.cs" -o -name "*.csproj" -o -name "*.sln" -o -name "*.json" -o -name "*.yaml" -o -name "*.yml" \) \
-exec sed -i 's/ServiceTemplate/PaymentService/g; s/service-template/payment-service/g' {} +
# 3. Rename files and directories
mv ServiceTemplate.sln PaymentService.sln
for d in src tests; do
find $d -name "ServiceTemplate.*" | while read f; do
mv "$f" "${f/ServiceTemplate/PaymentService}"
done
done
# 4. Re-init git history
rm -rf .git && git init && git add . && git commit -m "chore: initial commit from service-template"| Key | Default | Description |
|---|---|---|
ConnectionStrings:DefaultConnection |
Postgres on localhost:5432 | PostgreSQL connection string |
OpenTelemetry:ServiceName |
service-template |
Service name reported to OTEL |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4317 |
OTLP gRPC endpoint |
Serilog:MinimumLevel:Default |
Information |
Minimum log level |
Secrets (passwords, API keys) must never be committed. Use:
- Kubernetes Secrets (referenced in Helm chart)
- GitHub Actions secrets
.envfiles locally (already in.gitignore)