Home
cpnucleo
Cpnucleo is a project management and task tracking system built with .NET 10, demonstrating Clean Architecture, Domain-Driven Design, and a CQRS-like dual data access strategy with REST (FastEndpoints + EF Core) and gRPC (FastEndpoints Remote Messaging + Dapper) against the same PostgreSQL database.
Quick Links
| Page | Description |
|---|---|
| Architecture | Clean Architecture layers, CQRS dual implementation, DDD patterns |
| Getting Started | Prerequisites, build, run with Docker Compose or locally |
| Project Structure | Full tree of src/ and test/ with descriptions |
| API Reference | WebApi endpoints, IdentityApi auth, GrpcServer contracts |
| Database | PostgreSQL setup, EF Core, Dapper, init scripts |
| Testing | Architecture tests, unit tests, integration tests |
| Deployment | Docker Compose configs, GitHub Actions CI/CD, NGINX |
| Technologies | Full tech stack table with versions |
Key Features
- Clean Architecture with strict layer dependency enforcement validated by 25+ architecture tests
- Dual data access: EF Core for the REST API, Dapper with Unit of Work for the gRPC server
- FastEndpoints for both REST endpoints and gRPC-style remote command handling
- JWT authentication via the dedicated Identity API with PBKDF2-hashed credentials
- Rate limiting with fixed-window partitioning per IP (50/min WebApi, 10/min IdentityApi)
- OpenTelemetry observability with OTLP export and optional Grafana LGTM stack
- NGINX reverse proxy with least-connection load balancing across multiple WebApi instances
- Docker Compose configurations for development, default, and production environments
- AOT, Trim, and ExtraOptimize build options for production-optimized containers
- Blazor Server + WebAssembly frontend with MudBlazor UI components
- Automated CI/CD with GitHub Actions deploying to Azure Web Apps via GHCR
Repository
Api Reference
API Reference
Cpnucleo exposes three API services: the WebApi (REST), the IdentityApi (authentication), and the GrpcServer (gRPC command handling).
WebApi -- REST Endpoints
The WebApi uses FastEndpoints to define REST endpoints with Swagger/OpenAPI documentation. Each entity has 5 standard CRUD endpoints.
Base URL
- Direct:
http://localhost:5100 - Via NGINX load balancer:
http://localhost:9999
Swagger
Available in Development mode at /swagger.
Endpoint Pattern
Every entity follows this consistent pattern:
| Method | Route | Description |
|---|---|---|
POST |
/api/{entity} |
Create a new record |
GET |
/api/{entity}/{id} |
Get a single record by ID |
GET |
/api/{entity} |
List records (paginated) |
PUT |
/api/{entity}/{id} |
Update an existing record |
DELETE |
/api/{entity}/{id} |
Remove a record (soft delete) |
Available Entities
| Entity | Route Prefix | Tag |
|---|---|---|
| Appointment | /api/appointment |
Appointments |
| Assignment | /api/assignment |
Assignments |
| AssignmentImpediment | /api/assignmentimpediment |
AssignmentImpediments |
| AssignmentType | /api/assignmenttype |
AssignmentTypes |
| Impediment | /api/impediment |
Impediments |
| Organization | /api/organization |
Organizations |
| Project | /api/project |
Projects |
| User | /api/user |
Users |
| UserAssignment | /api/userassignment |
UserAssignments |
| UserProject | /api/userproject |
UserProjects |
| Workflow | /api/workflow |
Workflows |
Example: Create Appointment
Request:
POST /api/appointment
Content-Type: application/json
{
"id": "00000000-0000-0000-0000-000000000000",
"description": "Sprint planning meeting",
"keepDate": "2025-03-01T10:00:00Z",
"amountHours": 2,
"assignmentId": "...",
"userId": "..."
}
Response (200 OK):
{
"appointment": {
"id": "...",
"description": "Sprint planning meeting",
"keepDate": "2025-03-01T10:00:00Z",
"amountHours": 2,
"assignmentId": "...",
"userId": "...",
"createdAt": "...",
"active": true
}
}
Data Access
All WebApi endpoints use EF Core via IApplicationDbContext for database operations. Endpoints inject the context directly:
public class Endpoint(IApplicationDbContext dbContext) : Endpoint<Request, Response>
Rate Limiting
- 50 requests per minute per IP address
- Fixed-window partitioning
- Queue limit: 10 additional requests
- Returns
429 Too Many RequestswithRetry-After: 60header when exceeded
API Client Generation
The WebApi generates downloadable API clients via Kiota:
- C# client: Available at
/cs-client - TypeScript client: Generated during build
IdentityApi -- Authentication
Base URL
http://localhost:5200
Swagger
Available in Development mode at /swagger.
Login Endpoint
| Method | Route | Description |
|---|---|---|
POST |
/api/login |
Authenticate and receive JWT token |
Request:
POST /api/login
Content-Type: application/json
{
"login": "user@example.com",
"password": "password123"
}
Response (200 OK):
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}
Response (404 Not Found):
Returned when credentials are invalid.
JWT Configuration
| Parameter | Value |
|---|---|
| Issuer | https://identity.peris-studio.dev |
| Audience | https://peris-studio.dev |
| Expiration | 1 day |
| Algorithm | HMAC-SHA (via FastEndpoints.Security) |
Rate Limiting
- 10 requests per minute per IP address
- Queue limit: 3 additional requests
- Stricter than WebApi to protect against brute-force attacks
Output Caching
- Base policy: 10-second cache expiration
- All responses are cached by default
GrpcServer -- Remote Command Handling
The GrpcServer uses FastEndpoints.Messaging.Remote to handle commands over HTTP/2 gRPC transport.
Ports
- Health check:
http://localhost:5300/healthz(HTTP/1.1) - gRPC transport:
http://localhost:5301(HTTP/2)
Command Pattern
Each operation is a command/result pair defined in GrpcServer.Contracts:
Command -> Handler -> Result
Available Commands (per entity)
Each of the 11 entities has these 5 commands:
| Command | Description |
|---|---|
Create{Entity}Command |
Create a new record |
Get{Entity}ByIdCommand |
Retrieve by ID |
List{Entity}sCommand |
List with pagination |
Remove{Entity}Command |
Soft delete |
Update{Entity}Command |
Update fields |
Total: 55 registered command handlers.
Data Access
All gRPC handlers use Dapper via IUnitOfWork for database operations, providing transactional support with explicit BeginTransactionAsync, CommitAsync, and RollbackAsync.
Handler Registration
Handlers are registered in Program.cs:
app.MapHandlers(h =>
{
h.Register<CreateAppointmentCommand, CreateAppointmentHandler, CreateAppointmentResult>();
h.Register<GetAppointmentByIdCommand, GetAppointmentByIdHandler, GetAppointmentByIdResult>();
// ... 53 more handlers
});
Health Checks
All three APIs expose health check endpoints:
| Service | URL | Protocol |
|---|---|---|
| WebApi | /healthz |
HTTP |
| IdentityApi | /healthz |
HTTP |
| GrpcServer | /healthz |
HTTP |
Root Endpoint
All services also respond to GET / with "Hello World!".
Authentication Flow
JWT authentication is configured but currently commented out in WebApi and GrpcServer. The IdentityApi is fully functional for token generation. When enabled:
- Client authenticates via
POST /api/loginon IdentityApi - Receives JWT token
- Includes token in
Authorization: Bearer {token}header for WebApi/GrpcServer requests - Token validation checks issuer, audience, signing key, and expiration
Architecture
Architecture
Cpnucleo follows Clean Architecture principles with strict layer separation enforced by automated architecture tests (NetArchTest). The system implements a CQRS-like dual strategy where the REST API and gRPC server use different data access technologies against the same PostgreSQL database.
Layer Overview
+-------------------------------------------------------------+
| Presentation Layer |
| WebApi (REST) | GrpcServer (gRPC) | IdentityApi (Auth) |
| | | WebClient (Blazor) |
+-------------------------------------------------------------+
| Infrastructure Layer |
| EF Core (ApplicationDbContext) | Dapper (UnitOfWork) |
| NpgsqlConnection | Mappings | Migrations |
+-------------------------------------------------------------+
| Domain Layer |
| Entities | Repositories (Interfaces) | UoW (Interface) |
| Models | Common (Security) |
+-------------------------------------------------------------+
Domain Layer (src/Domain)
The innermost layer with zero external dependencies. Architecture tests verify that Domain does not depend on EF Core, Dapper, Npgsql, or any presentation project.
Entities
All entities inherit from BaseEntity, which provides:
public abstract class BaseEntity
{
public Guid Id { get; protected init; }
public DateTime CreatedAt { get; protected init; }
public DateTime? UpdatedAt { get; protected set; }
public DateTime? DeletedAt { get; protected set; }
public bool Active { get; protected set; }
}
Each entity is sealed and uses static factory methods for creation, updates, and soft deletes:
Create(...)-- initializes a new entity withActive = trueUpdate(...)-- modifies fields and setsUpdatedAtRemove(...)-- setsActive = falseandDeletedAt(soft delete)
Entities are annotated with [Table("...")] for Dapper's advanced repository to resolve table names.
Domain Entities
| Entity | Key Fields | Relationships |
|---|---|---|
| Organization | Name, Description | Parent of Projects |
| Project | Name | Belongs to Organization |
| Assignment | Name, Description, StartDate, EndDate, AmountHours | Belongs to Project, Workflow, User, AssignmentType |
| AssignmentType | Name | Referenced by Assignments |
| Workflow | Name, Order | Referenced by Assignments |
| User | Name, Login, Password, Salt | PBKDF2-encrypted credentials |
| Appointment | Description, KeepDate, AmountHours | Belongs to Assignment, User |
| Impediment | Name | Referenced by AssignmentImpediments |
| AssignmentImpediment | Description | Links Assignment to Impediment |
| UserAssignment | -- | Many-to-many: User to Assignment |
| UserProject | -- | Many-to-many: User to Project |
Repository Interfaces
IRepository<T>-- generic CRUD:GetByIdAsync,GetAllAsync(paginated),AddAsync,UpdateAsync,DeleteAsync,ExistsAsyncIProjectRepository-- specialized repository for Project-specific queriesIUnitOfWork-- transaction management:BeginTransactionAsync,CommitAsync,RollbackAsync,GetRepository<T>
Infrastructure Layer (src/Infrastructure)
Implements data access with two strategies side by side:
EF Core (used by WebApi and IdentityApi)
ApplicationDbContextwithIApplicationDbContextinterface- DbSet properties for all entities
- Migrations generated from EF Core, applied via SQL init scripts in Docker
- Delta middleware for HTTP conditional requests based on database timestamps
Dapper (used by GrpcServer)
DapperRepository<T>-- generic repository using raw SQL with reflection-based column mapping- Caches
PropertyInfo[]viaLazy<>for performance - Supports paginated queries with configurable sort column/order and SQL injection protection
UnitOfWorkwrapsNpgsqlConnection+NpgsqlTransactionfor transactional operationsDapper.AOTenabled for compile-time SQL interception
Dependency Injection
DependencyInjection.AddInfrastructure() registers:
IApplicationDbContextasApplicationDbContext(EF Core, scoped)NpgsqlConnection(Dapper basic, scoped)IProjectRepositoryasProjectRepository(Dapper specialized, scoped)IUnitOfWorkasUnitOfWork(Dapper advanced with transactions, scoped)- Optional fake data generation via Bogus when
CreateFakeDatais configured
Presentation Layer
WebApi (src/WebApi)
- FastEndpoints for REST API with Swagger/OpenAPI documentation
- Uses EF Core via
IApplicationDbContextfor data access - Rate limiting: 50 requests/minute per IP with fixed-window partitioning
- Kiota-based API client generation (C# and TypeScript)
- Middleware:
ElapsedTimeMiddleware,ErrorHandlingMiddleware - Riok.Mapperly for compile-time DTO mapping
GrpcServer (src/GrpcServer)
- FastEndpoints.Messaging.Remote for gRPC-style command/handler pattern
- Uses Dapper via
IUnitOfWorkfor data access - HTTP/2 on port 5021 for gRPC transport
- Command/Result pattern via
GrpcServer.Contracts - All 11 entities have 5 handlers each: Create, GetById, List, Remove, Update (55 handlers total)
IdentityApi (src/IdentityApi)
- JWT token generation via
FastEndpoints.Security - Login endpoint authenticating against User credentials in the database
- Output caching with 10-second base policy
- Rate limiting: 10 requests/minute per IP
- Swagger/OpenAPI documentation
WebClient (src/WebClient)
- Blazor Server + WebAssembly hybrid rendering
- MudBlazor UI component library with translations
- Interactive server and WebAssembly render modes
- Static asset serving
CQRS Dual Implementation
The system demonstrates two parallel approaches to the same domain:
| Aspect | WebApi (REST) | GrpcServer (gRPC) |
|---|---|---|
| Framework | FastEndpoints | FastEndpoints.Messaging.Remote |
| Data Access | EF Core + ApplicationDbContext | Dapper + UnitOfWork |
| Transport | HTTP/1.1 REST | HTTP/2 gRPC |
| Internal Port | 5000 | 5021 |
| Load Balanced | Yes (NGINX, 2 instances) | No |
Both implementations share the same Domain entities and PostgreSQL database.
Dependency Rules (Enforced by Architecture Tests)
- Domain depends on nothing (no EF Core, no Dapper, no Npgsql)
- Infrastructure depends only on Domain
- GrpcServer.Contracts depends only on Domain
- WebApi does not depend on GrpcServer
- IdentityApi does not depend on GrpcServer
- All entities must inherit from
BaseEntityand besealed - All repository interfaces must start with
I - All endpoints must be named
Endpoint - All gRPC handlers must end with
Handler - All commands must end with
Command - All DTOs must end with
Dto
Database
Database
Cpnucleo uses PostgreSQL 16.7 as its primary database, accessed via two parallel data access strategies: EF Core (for the REST API) and Dapper (for the gRPC server).
Database Setup
Docker (Automatic)
The database is automatically provisioned when running with Docker Compose. The db service:
- Starts PostgreSQL 16.7
- Creates the database using credentials from
.env - Runs SQL scripts from
docker-entrypoint-initdb.d/in alphabetical order
Docker Configuration
db:
image: postgres:16.7
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- db_data:/var/lib/postgresql/data
- ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
command: >
postgres
-c checkpoint_timeout=600
-c max_wal_size=4096
-c synchronous_commit=0
-c fsync=0
-c full_page_writes=0
Performance flags (optimized for development speed over durability):
checkpoint_timeout=600-- less frequent checkpointsmax_wal_size=4096-- larger WAL before checkpointsynchronous_commit=0-- async commitsfsync=0-- skip fsync (data loss risk, faster writes)full_page_writes=0-- skip full-page writes
Manual Setup
psql -U postgres -f docker-entrypoint-initdb.d/001-track-commit-timestamp.sql
psql -U postgres -d cpnucleo -f docker-entrypoint-initdb.d/002-database-dump-ddl.sql
Initialization Scripts
001-track-commit-timestamp.sql
Enables commit timestamp tracking for the Delta middleware:
ALTER SYSTEM SET track_commit_timestamp = on;
This allows the Delta library to implement HTTP conditional requests based on when data was last modified.
002-database-dump-ddl.sql
Contains the full DDL schema generated from EF Core migrations. Creates all tables, constraints, and indexes idempotently (using IF NOT EXISTS checks).
Schema
Tables
| Table | Primary Key | Key Columns | Foreign Keys |
|---|---|---|---|
| Organizations | Id (uuid) | Name, Description | -- |
| Projects | Id (uuid) | Name | OrganizationId -> Organizations |
| Assignments | Id (uuid) | Name, Description, StartDate, EndDate, AmountHours | ProjectId -> Projects, WorkflowId -> Workflows, UserId -> Users, AssignmentTypeId -> AssignmentTypes |
| AssignmentTypes | Id (uuid) | Name | -- |
| Workflows | Id (uuid) | Name, Order | -- |
| Users | Id (uuid) | Name, Login, Password, Salt | -- |
| Appointments | Id (uuid) | Description, KeepDate, AmountHours | AssignmentId -> Assignments, UserId -> Users |
| Impediments | Id (uuid) | Name | -- |
| AssignmentImpediments | Id (uuid) | Description | AssignmentId -> Assignments, ImpedimentId -> Impediments |
| UserAssignments | Id (uuid) | -- | UserId -> Users, AssignmentId -> Assignments |
| UserProjects | Id (uuid) | -- | UserId -> Users, ProjectId -> Projects |
Common Columns (all tables)
| Column | Type | Description |
|---|---|---|
| Id | uuid | Primary key (generated via Guid.NewGuid()) |
| CreatedAt | timestamp with time zone | Record creation time |
| UpdatedAt | timestamp with time zone (nullable) | Last update time |
| DeletedAt | timestamp with time zone (nullable) | Soft delete time |
| Active | boolean | Soft delete flag (true = active) |
Indexes
All tables have indexes on:
CreatedAt-- for Delta middleware timestamp queries- Foreign key columns -- for join performance
Connection Configuration
Connection String
Configured via the DB_CONNECTION_STRING environment variable:
Host=db;Username=postgres;Password=postgres;Database=cpnucleo;Minimum Pool Size=10;Maximum Pool Size=10;Multiplexing=true
| Parameter | Value | Purpose |
|---|---|---|
| Host | db (Docker) / localhost (local) |
Database server |
| Minimum Pool Size | 10 | Pre-allocated connections |
| Maximum Pool Size | 10 | Connection limit |
| Multiplexing | true | Npgsql multiplexing for better throughput |
EF Core (WebApi + IdentityApi)
ApplicationDbContext
The ApplicationDbContext implements IApplicationDbContext and provides DbSet properties for all 11 entities. It is registered as a scoped service.
Migrations
EF Core migrations are maintained in src/Infrastructure/Migrations/. The initial migration 20250219224724_InitiaDblMigration creates the full schema.
For production, migrations are exported as SQL and placed in docker-entrypoint-initdb.d/ rather than running EF Core migrations at startup.
Delta Middleware
The Delta library is integrated for HTTP conditional requests:
app.UseDelta(
getConnection: httpContext => httpContext.RequestServices.GetRequiredService<NpgsqlConnection>());
This enables If-Modified-Since / 304 Not Modified responses using PostgreSQL's commit timestamps.
Dapper (GrpcServer)
Generic Repository
DapperRepository<T> provides CRUD operations via raw SQL:
GetByIdAsync--SELECT * FROM "Table" WHERE "Id" = @Id AND "Active" = trueGetAllAsync-- Paginated query withOFFSET/LIMIT, configurable sort column with SQL injection protectionAddAsync--INSERT INTO ... RETURNING "Id"with reflection-based column mappingUpdateAsync--UPDATE ... SET ... WHERE "Id" = @IdDeleteAsync-- Hard delete (for gRPC operations)ExistsAsync--SELECT EXISTS(...)check
Performance Optimizations
PropertyInfo[]cached viaLazy<>to avoid repeated reflectionHashSet<string>for O(1) sort column validationDapper.AOTfor compile-time SQL interception
Unit of Work
UnitOfWork wraps NpgsqlConnection and NpgsqlTransaction:
public interface IUnitOfWork
{
IRepository<T> GetRepository<T>() where T : BaseEntity;
Task BeginTransactionAsync();
Task CommitAsync(CancellationToken cancellationToken = default);
Task RollbackAsync(CancellationToken cancellationToken = default);
}
Specialized Repository
ProjectRepository implements IProjectRepository for project-specific queries that go beyond the generic CRUD operations.
Fake Data Generation
The Infrastructure layer includes a FakeDataHelper that uses the Bogus library to generate realistic test data. When CreateFakeData=true is set in configuration:
- Bogus generates fake data for all entities
- Outputs SQL/CSV dump files
- Files should be placed in
docker-entrypoint-initdb.d/for seeding
Deployment
Deployment
Cpnucleo uses Docker Compose for containerized deployment and GitHub Actions for CI/CD, with final deployment to Azure Web Apps.
Docker Compose Configurations
The project provides three compose configurations that can be layered:
Base (compose.yaml)
The base configuration defines all services with pre-built GHCR images:
| Service | Image | Internal Port | External Port |
|---|---|---|---|
| webapi1-cpnucleo | ghcr.io/jonathanperis/cpnucleo-web-api:latest | 5000 | 5100 |
| webapi2-cpnucleo | ghcr.io/jonathanperis/cpnucleo-web-api:latest | 5000 | 5111 |
| identityapi-cpnucleo | ghcr.io/jonathanperis/cpnucleo-identity-api:latest | 5010 | 5200 |
| grpcserver-cpnucleo | ghcr.io/jonathanperis/cpnucleo-grpc-server:latest | 5020/5021 | 5300/5301 |
| webclient-cpnucleo | ghcr.io/jonathanperis/cpnucleo-web-client:latest | 5030 | 5400 |
| db | postgres:16.7 | 5432 | 5432 |
| nginx | nginx | 9999 | 9999 |
All API services depend on db being healthy before starting.
Development Override (compose.override.yaml)
docker compose -f compose.yaml -f compose.override.yaml up --build
Differences from base:
- Builds from source using Dockerfiles in
src/ - Build args:
AOT=false,TRIM=false,EXTRA_OPTIMIZE=false,BUILD_CONFIGURATION=Debug - Adds Grafana LGTM OpenTelemetry stack (ports 3000, 4317, 4318)
- Resource limits: 0.4 CPU, 100MB memory per service
Production Override (compose.prod.yaml)
docker compose -f compose.yaml -f compose.prod.yaml up -d
Differences from base:
restart: alwayson all services- Resource reservations: 0.25 CPU / 256MB per API, 0.50 CPU / 512MB per DB
- Resource limits: 0.50 CPU / 512MB per API, 1.0 CPU / 1GB for DB
- JSON logging with rotation: 10MB max size, 3 files retained
- No build step (uses pre-built images)
Dockerfiles
Each service has a multi-stage Dockerfile supporting configurable build options:
Build Arguments
| Argument | Description | Dev Value | Prod Value |
|---|---|---|---|
AOT |
Enable Native AOT compilation | false | false |
TRIM |
Enable assembly trimming with ReadyToRun | false | true |
EXTRA_OPTIMIZE |
Aggressive optimizations (remove symbols, disable debugger, invariant globalization) | false | true |
BUILD_CONFIGURATION |
.NET build configuration | Debug | Release |
ASPNETCORE_ENVIRONMENT |
Runtime environment | Development | Production |
DB_CONNECTION_STRING |
Database connection string | (from .env) | (from secrets) |
Build Stages
- base --
mcr.microsoft.com/dotnet/aspnet:10.0runtime image - build --
mcr.microsoft.com/dotnet/sdk:10.0with clang/zlib for AOT support; restores, builds - publish -- Publishes with configured optimizations
- final -- Copies published output to runtime image
Platform Support
All images are built for linux/amd64 and linux/arm64/v8.
NGINX Reverse Proxy
NGINX load-balances traffic across two WebApi instances:
upstream api {
least_conn;
server webapi1-cpnucleo:5000;
server webapi2-cpnucleo:5000;
}
server {
listen 9999;
location / {
proxy_pass http://api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Configuration Highlights
- least_conn load balancing -- sends requests to the server with fewest active connections
- gzip compression -- level 5, minimum 256 bytes
- keepalive_timeout: 0 -- persistent connections disabled for stateless APIs
- server_tokens: off -- hides NGINX version
- access_log: off -- disabled for performance
- epoll event model with multi_accept
GitHub Actions CI/CD
Build Check (build-check.yml)
Triggered on pull requests to main.
Jobs:
Setup, Build & Test (matrix: WebApi, GrpcServer, IdentityApi, WebClient)
- Checkout repository
- Setup .NET SDK (from global.json)
- Restore dependencies
- Build application (Debug, no AOT/Trim)
- Run Architecture Tests
Container Healthcheck Test (depends on build)
- Build Docker image from source
- Start container via Docker Compose
- Poll
/healthzendpoint up to 20 times with 5-second intervals - Fail if health check does not return 200
Main Release (main-release.yml)
Triggered on push to main and manual dispatch.
Jobs:
Setup, Build & Test -- Same as build check but with
TRIM=true,EXTRA_OPTIMIZE=true,BUILD_CONFIGURATION=ReleaseBuild & Push Docker Image (depends on test)
- Setup QEMU and Docker Buildx for multi-platform builds
- Login to GHCR
- Build and push images for
linux/amd64andlinux/arm64/v8 - Images pushed to GHCR:
ghcr.io/jonathanperis/cpnucleo-{service}:latest
Container Healthcheck Test (depends on push)
- Pull production images
- Run healthcheck validation
Deploy to Azure (depends on healthcheck)
- Deploy each service to its Azure Web App
- Uses publish profiles stored in GitHub Secrets
Azure Deployment Targets
| Service | Azure Web App Name | Environment |
|---|---|---|
| WebApi | cpnucleo-api-dotnet | production-webapi |
| GrpcServer | cpnucleo-grpc-server | production-grpcserver |
| IdentityApi | cpnucleo-identity-api | production-identityapi |
| WebClient | cpnucleo-webclient-dotnet | production-webclient |
Environment Variables
Required (.env)
| Variable | Description | Example |
|---|---|---|
POSTGRES_USER |
PostgreSQL username | postgres |
POSTGRES_PASSWORD |
PostgreSQL password | postgres |
POSTGRES_DB |
Database name | cpnucleo |
DB_CONNECTION_STRING |
Full Npgsql connection string | Host=db;Username=postgres;... |
OTEL_EXPORTER_OTLP_ENDPOINT |
OpenTelemetry collector endpoint | http://otel-lgtm:4317 |
OTEL_METRIC_EXPORT_INTERVAL |
Metric export interval (ms) | 5000 |
GitHub Secrets (for CI/CD)
| Secret | Purpose |
|---|---|
GITHUB_TOKEN |
GHCR authentication (automatic) |
DB_CONNECTION_STRING |
Production database connection |
AZURE_WEBAPP_PUBLISH_PROFILE_WEBAPI |
Azure publish profile for WebApi |
AZURE_WEBAPP_PUBLISH_PROFILE_GRPCSERVER |
Azure publish profile for GrpcServer |
AZURE_WEBAPP_PUBLISH_PROFILE_IDENTITYAPI |
Azure publish profile for IdentityApi |
AZURE_WEBAPP_PUBLISH_PROFILE_WEBCLIENT |
Azure publish profile for WebClient |
Network
All services communicate over a shared Docker bridge network:
networks:
default:
name: cpnucleo_network
driver: bridge
Service discovery uses Docker DNS (e.g., db, webapi1-cpnucleo).
Getting Started
Getting Started
Prerequisites
| Tool | Version | Notes |
|---|---|---|
| .NET SDK | 10.0.102+ | Specified in global.json with latestMinor roll-forward |
| Docker | Latest | For running with Docker Compose |
| Docker Compose | v2+ | Bundled with Docker Desktop |
| PostgreSQL | 16.7 | Provided via Docker; only needed if running locally without Docker |
Clone the Repository
git clone https://github.com/jonathanperis/cpnucleo.git
cd cpnucleo
Run with Docker Compose (Recommended)
Default Mode (Pre-built Images)
Uses pre-built images from GHCR:
docker compose up
Development Mode
Builds from source with debug configuration and includes an OpenTelemetry/Grafana LGTM stack for observability:
docker compose -f compose.yaml -f compose.override.yaml up --build
Production Mode
Uses pre-built images with resource reservations, restart policies, and structured JSON logging:
docker compose -f compose.yaml -f compose.prod.yaml up -d
Services and Ports
Once running, the services are available at:
| Service | URL | Description |
|---|---|---|
| WebApi (instance 1) | http://localhost:5100 | REST API |
| WebApi (instance 2) | http://localhost:5111 | REST API (load-balanced pair) |
| IdentityApi | http://localhost:5200 | JWT Authentication API |
| GrpcServer | http://localhost:5300 (health) / :5301 (gRPC) | gRPC command server |
| WebClient | http://localhost:5400 | Blazor UI |
| NGINX | http://localhost:9999 | Reverse proxy (load balances WebApi) |
| PostgreSQL | localhost:5432 | Database |
| Grafana (dev only) | http://localhost:3000 | Observability dashboard |
| OTLP Collector (dev only) | localhost:4317 (gRPC) / :4318 (HTTP) | OpenTelemetry collector |
Health Checks
All services expose a health endpoint:
curl http://localhost:5100/healthz # WebApi
curl http://localhost:5200/healthz # IdentityApi
curl http://localhost:5300/healthz # GrpcServer
curl http://localhost:5400/healthz # WebClient
Run Locally (Without Docker)
1. Start PostgreSQL
Ensure PostgreSQL 16+ is running locally. Execute the init scripts to set up the schema:
psql -U postgres -f docker-entrypoint-initdb.d/001-track-commit-timestamp.sql
psql -U postgres -d cpnucleo -f docker-entrypoint-initdb.d/002-database-dump-ddl.sql
2. Configure Environment
The project uses environment variables loaded from the .env file. For local development, update the connection string to point to localhost:
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=cpnucleo
DB_CONNECTION_STRING=Host=localhost;Username=postgres;Password=postgres;Database=cpnucleo;Minimum Pool Size=10;Maximum Pool Size=10;Multiplexing=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_METRIC_EXPORT_INTERVAL=5000
3. Build the Solution
dotnet build cpnucleo.slnx
4. Run the Services
Each service must be run in a separate terminal:
# Terminal 1 - REST API
cd src/WebApi && dotnet run
# Terminal 2 - Identity/Auth API
cd src/IdentityApi && dotnet run
# Terminal 3 - gRPC Server
cd src/GrpcServer && dotnet run
# Terminal 4 - Blazor Web Client
cd src/WebClient && dotnet run
Swagger UI
When running in Development mode, Swagger UI is available for interactive API exploration:
- WebApi: http://localhost:5100/swagger
- IdentityApi: http://localhost:5200/swagger
Generate Fake Data
Set CreateFakeData=true in the application configuration to generate CSV/SQL dump files using Bogus. The generated files should be placed in docker-entrypoint-initdb.d/ for automatic database seeding on container startup.
Project Structure
Project Structure
Solution Overview
cpnucleo/
├── cpnucleo.slnx # Solution file
├── global.json # .NET SDK version (10.0.102)
├── compose.yaml # Docker Compose (default/base)
├── compose.override.yaml # Docker Compose (development overrides)
├── compose.prod.yaml # Docker Compose (production overrides)
├── nginx.conf # NGINX reverse proxy configuration
├── .env # Environment variables
├── docker-entrypoint-initdb.d/ # PostgreSQL initialization scripts
├── .github/workflows/ # CI/CD pipelines
├── src/ # Source code
└── test/ # Test projects
Source Projects (src/)
Domain (src/Domain/)
The core business layer with zero external dependencies.
Domain/
├── Domain.csproj # No external NuGet packages
├── Usings.cs # Global usings
├── Common/
│ └── Security/
│ └── CryptographyManager.cs # PBKDF2 password hashing
├── Entities/
│ ├── BaseEntity.cs # Abstract base (Id, CreatedAt, UpdatedAt, DeletedAt, Active)
│ ├── Appointment.cs # Time tracking entries
│ ├── Assignment.cs # Tasks/work items
│ ├── AssignmentImpediment.cs # Links assignments to impediments
│ ├── AssignmentType.cs # Task categorization
│ ├── Impediment.cs # Blockers/obstacles
│ ├── Organization.cs # Top-level organizational unit
│ ├── Project.cs # Projects within organizations
│ ├── User.cs # System users with encrypted credentials
│ ├── UserAssignment.cs # User-to-assignment mapping (many-to-many)
│ ├── UserProject.cs # User-to-project mapping (many-to-many)
│ └── Workflow.cs # Workflow stages with ordering
├── Models/
│ ├── PaginatedResult.cs # Generic paginated response model
│ └── PaginationParams.cs # Pagination request parameters
├── Repositories/
│ ├── IRepository.cs # Generic CRUD repository interface
│ └── IProjectRepository.cs # Specialized project repository
└── UoW/
└── IUnitOfWork.cs # Unit of Work interface for transactions
Infrastructure (src/Infrastructure/)
Data access implementations using both EF Core and Dapper.
Infrastructure/
├── Infrastructure.csproj # EF Core, Dapper, Dapper.AOT, Npgsql, Bogus, Delta
├── DependencyInjection.cs # Service registration for all data access
├── Usings.cs
├── Common/
│ ├── Context/
│ │ ├── ApplicationDbContext.cs # EF Core DbContext implementation
│ │ └── IApplicationDbContext.cs # DbContext interface
│ ├── Helpers/
│ │ └── FakeDataHelper.cs # Bogus-based test data generator
│ └── Mappings/
│ └── ... # EF Core entity configurations
├── Migrations/
│ └── ... # EF Core database migrations
├── Repositories/
│ ├── DapperRepository.cs # Generic Dapper CRUD repository
│ └── ProjectRepository.cs # Specialized Dapper project repository
└── UoW/
└── UnitOfWork.cs # Dapper-based Unit of Work with transactions
WebApi (src/WebApi/)
REST API using FastEndpoints with EF Core data access.
WebApi/
├── WebApi.csproj # FastEndpoints, Swagger, Mapperly, OpenTelemetry
├── Program.cs # App configuration (rate limiting, health checks, Swagger)
├── AssemblyInfo.cs
├── Usings.cs
├── Dockerfile # Multi-stage build with AOT/Trim support
├── Common/
│ └── Dtos/ # Data transfer objects
├── Endpoints/
│ ├── Appointment/
│ │ ├── CreateAppointment/ # POST /api/appointment
│ │ │ ├── Endpoint.cs
│ │ │ └── Models.cs # Request/Response models
│ │ ├── GetAppointmentById/ # GET /api/appointment/{id}
│ │ ├── ListAppointments/ # GET /api/appointment
│ │ ├── RemoveAppointment/ # DELETE /api/appointment/{id}
│ │ └── UpdateAppointment/ # PUT /api/appointment/{id}
│ ├── Assignment/ # Same 5 CRUD endpoints
│ ├── AssignmentImpediment/
│ ├── AssignmentType/
│ ├── Impediment/
│ ├── Organization/
│ ├── Project/
│ ├── User/
│ ├── UserAssignment/
│ ├── UserProject/
│ └── Workflow/
├── Middlewares/
│ ├── ElapsedTimeMiddleware.cs # Request timing
│ └── ErrorHandlingMiddleware.cs # Global error handling
├── Properties/
│ └── launchSettings.json
├── ServiceExtensions/
│ └── ... # OpenTelemetry configuration
├── appsettings.json
├── appsettings.Development.json
└── appsettings.Testing.json
GrpcServer (src/GrpcServer/)
gRPC command server using FastEndpoints Remote Messaging with Dapper data access.
GrpcServer/
├── GrpcServer.csproj # FastEndpoints.Messaging.Remote, Mapperly, OpenTelemetry
├── Program.cs # HTTP/2 on port 5021, handler registration (55 handlers)
├── Usings.cs
├── Dockerfile
├── Common/
│ └── Dtos/ # Data transfer objects
├── Handlers/
│ ├── Appointment/
│ │ ├── CreateAppointmentHandler.cs
│ │ ├── GetAppointmentByIdHandler.cs
│ │ ├── ListAppointmentsHandler.cs
│ │ ├── RemoveAppointmentHandler.cs
│ │ └── UpdateAppointmentHandler.cs
│ ├── Assignment/ # Same 5 handlers per entity
│ ├── AssignmentImpediment/
│ ├── AssignmentType/
│ ├── Impediment/
│ ├── Organization/
│ ├── Project/
│ ├── User/
│ ├── UserAssignment/
│ ├── UserProject/
│ └── Workflow/
├── Properties/
│ └── launchSettings.json
├── ServiceExtensions/
│ └── ... # OpenTelemetry configuration
├── appsettings.json
└── appsettings.Development.json
GrpcServer.Contracts (src/GrpcServer.Contracts/)
Shared command/result contracts between gRPC client and server.
GrpcServer.Contracts/
├── GrpcServer.Contracts.csproj # FastEndpoints.Messaging.Core, Domain reference
├── Usings.cs
├── Common/
│ └── Dtos/ # Shared DTOs
└── Commands/
├── Appointment/ # CreateAppointmentCommand, GetAppointmentByIdCommand, etc.
├── Assignment/
├── AssignmentImpediment/
├── AssignmentType/
├── Impediment/
├── Organization/
├── Project/
├── User/
├── UserAssignment/
├── UserProject/
└── Workflow/
IdentityApi (src/IdentityApi/)
JWT authentication service.
IdentityApi/
├── IdentityApi.csproj # FastEndpoints, FastEndpoints.Security, Swagger, OpenTelemetry
├── Program.cs # JWT config, rate limiting (10/min), output caching
├── Usings.cs
├── Dockerfile
├── Endpoints/
│ └── Login/
│ ├── Endpoint.cs # POST /api/login
│ └── Models.cs # Request (Login, Password) / Response (Token)
├── Middlewares/
│ ├── ElapsedTimeMiddleware.cs
│ └── ErrorHandlingMiddleware.cs
├── Properties/
│ └── launchSettings.json
├── ServiceExtensions/
│ └── ... # OpenTelemetry configuration
├── appsettings.json
└── appsettings.Development.json
WebClient (src/WebClient/)
Blazor Server + WebAssembly frontend.
WebClient/
├── WebClient.csproj # MudBlazor, MudBlazor.Translations, OpenTelemetry
├── Program.cs # Blazor hybrid rendering, MudBlazor services
├── Usings.cs
├── Dockerfile
├── Components/
│ └── ... # Blazor components
├── Properties/
│ └── launchSettings.json
├── ServiceExtensions/
│ └── ... # OpenTelemetry configuration
├── wwwroot/
│ └── ... # Static assets
├── appsettings.json
└── appsettings.Development.json
Test Projects (test/)
Architecture.Tests (test/Architecture.Tests/)
Validates Clean Architecture dependency rules using NetArchTest.
Architecture.Tests/
├── Architecture.Tests.csproj # xUnit, NetArchTest.Rules, FluentAssertions
├── ArchitectureTests.cs # 25+ architecture validation tests
├── Usings.cs
└── README.md
WebApi.Unit.Tests (test/WebApi.Unit.Tests/)
Unit tests for WebApi endpoints.
WebApi.Unit.Tests/
├── WebApi.Unit.Tests.csproj # NUnit, FakeItEasy, Shouldly, FastEndpoints
├── Endpoints/
│ └── ... # Endpoint unit tests
├── Usings.cs
└── README.md
WebApi.Integration.Tests (test/WebApi.Integration.Tests/)
Integration tests running against real services.
WebApi.Integration.Tests/
├── WebApi.Integration.Tests.csproj # xUnit v3, FastEndpoints.Testing, Shouldly
├── AssemblyInfo.cs
├── Endpoints/
│ └── ... # Endpoint integration tests
├── Hosts/
│ └── ... # Test host configuration
└── Usings.cs
Configuration Files
| File | Purpose |
|---|---|
compose.yaml |
Base Docker Compose with all services, PostgreSQL, NGINX |
compose.override.yaml |
Development overrides: build from source, Grafana LGTM |
compose.prod.yaml |
Production: resource limits/reservations, restart policies, logging |
nginx.conf |
NGINX reverse proxy with least-conn load balancing |
.env |
Database credentials, connection string, OTEL config |
global.json |
.NET SDK version pinning |
docker-entrypoint-initdb.d/ |
SQL scripts run on PostgreSQL container startup |
Technologies
Technologies
Runtime & Framework
| Technology | Version | Purpose |
|---|---|---|
| .NET | 10.0 | Runtime and SDK |
| ASP.NET Core | 10.0 | Web framework |
| C# | Latest (via LangVersion) | Programming language |
Web Frameworks & API
| Technology | Version | Purpose |
|---|---|---|
| FastEndpoints | 7.2.0 | REST endpoint framework (WebApi, IdentityApi) |
| FastEndpoints.Swagger | 7.2.0 | OpenAPI/Swagger documentation |
| FastEndpoints.Security | 7.2.0 | JWT token generation and validation (IdentityApi) |
| FastEndpoints.Messaging.Remote | 7.2.0 | gRPC-style remote command handling (GrpcServer) |
| FastEndpoints.Messaging.Core | 7.2.0 | Shared command/result contracts (GrpcServer.Contracts) |
| FastEndpoints.Generator | 7.2.0 | Source generator for endpoint discovery |
| FastEndpoints.ClientGen.Kiota | 8.1.0 | API client generation (C#, TypeScript) |
| FastEndpoints.Testing | 7.2.0 | Integration test support |
Data Access
| Technology | Version | Purpose |
|---|---|---|
| Entity Framework Core | 10.0.3 | ORM for WebApi and IdentityApi |
| EF Core Design | 10.0.3 | Migration tooling |
| Npgsql | 10.0.1 | PostgreSQL .NET driver |
| Npgsql.EntityFrameworkCore.PostgreSQL | 10.0.0 | EF Core PostgreSQL provider |
| Dapper | 2.1.72 | Micro-ORM for GrpcServer |
| Dapper.AOT | 1.0.48 | Compile-time SQL interception |
| Delta | 9.0.0 | HTTP conditional requests via DB timestamps |
Database
| Technology | Version | Purpose |
|---|---|---|
| PostgreSQL | 16.7 | Primary database |
Authentication
| Technology | Version | Purpose |
|---|---|---|
| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.3 | JWT Bearer authentication middleware |
Mapping
| Technology | Version | Purpose |
|---|---|---|
| Riok.Mapperly | 4.3.1 | Compile-time object mapping (source generator) |
Querying
| Technology | Version | Purpose |
|---|---|---|
| System.Linq.Dynamic.Core | 1.7.1 | Dynamic LINQ queries |
Observability & Monitoring
| Technology | Version | Purpose |
|---|---|---|
| OpenTelemetry.Exporter.Console | 1.15.1 | Console telemetry export |
| OpenTelemetry.Exporter.OpenTelemetryProtocol | 1.15.0 | OTLP telemetry export |
| OpenTelemetry.Extensions.Hosting | 1.15.0 | Host integration |
| OpenTelemetry.Instrumentation.AspNetCore | 1.15.0 | ASP.NET Core instrumentation |
| OpenTelemetry.Instrumentation.Http | 1.15.0 | HTTP client instrumentation |
| OpenTelemetry.Instrumentation.Process | 1.12.0-beta.1 | Process metrics |
| OpenTelemetry.Instrumentation.Runtime | 1.15.0 | .NET runtime metrics |
| Grafana LGTM | Latest | Observability stack (dev only, via Docker) |
Frontend
| Technology | Version | Purpose |
|---|---|---|
| Blazor Server | 10.0 | Server-side interactive rendering |
| Blazor WebAssembly | 10.0 | Client-side interactive rendering |
| MudBlazor | 8.x | Material Design UI component library |
| MudBlazor.Translations | 2.x | MudBlazor localization support |
Testing
| Technology | Version | Purpose |
|---|---|---|
| xUnit | 2.9.x | Test framework (Architecture.Tests) |
| xUnit v3 | 3.x | Test framework (Integration.Tests) |
| NUnit | 4.x | Test framework (Unit.Tests) |
| NetArchTest.Rules | 1.3.2 | Architecture rule validation |
| FluentAssertions | 8.x | Fluent assertion library |
| FakeItEasy | 9.x | Mocking framework |
| Shouldly | 4.x | Assertion library |
| Bogus | 35.x | Fake data generation |
| coverlet.collector | 6.x | Code coverage collection |
| Microsoft.NET.Test.Sdk | 18.x | .NET test infrastructure |
Infrastructure & DevOps
| Technology | Version | Purpose |
|---|---|---|
| Docker | Latest | Containerization |
| Docker Compose | v2 | Multi-container orchestration |
| NGINX | Latest | Reverse proxy and load balancer |
| GitHub Actions | -- | CI/CD pipelines |
| Azure Web Apps | -- | Cloud hosting (production deployment target) |
| GHCR | -- | Container image registry |
Build Optimization
| Feature | Description |
|---|---|
| PublishAot | Native AOT compilation (optional) |
| PublishReadyToRun | ReadyToRun pre-compilation |
| PublishReadyToRunComposite | Composite R2R for better startup |
| InvariantGlobalization | Reduce binary size by removing culture data |
| TrimmerRemoveSymbols | Strip debug symbols |
| Multi-platform | linux/amd64 and linux/arm64/v8 |
Testing
Testing
Cpnucleo has three test projects covering architecture validation, unit testing, and integration testing.
Test Projects Overview
| Project | Framework | Focus | Key Libraries |
|---|---|---|---|
| Architecture.Tests | xUnit | Clean Architecture rules | NetArchTest.Rules, FluentAssertions |
| WebApi.Unit.Tests | NUnit | Endpoint unit tests | FakeItEasy, Shouldly, FastEndpoints |
| WebApi.Integration.Tests | xUnit v3 | End-to-end endpoint tests | FastEndpoints.Testing, Shouldly |
Architecture Tests (test/Architecture.Tests/)
These tests enforce Clean Architecture dependency rules at build time using NetArchTest and FluentAssertions. They run as part of both the PR build check and the release pipeline.
Layer Dependency Tests
| Test | Rule |
|---|---|
Domain_Should_Not_HaveDependencyOnOtherProjects |
Domain has no dependency on Infrastructure, WebApi, IdentityApi, GrpcServer, GrpcServer.Contracts, or WebClient |
Infrastructure_Should_Not_HaveDependencyOnOtherProjects |
Infrastructure has no dependency on WebApi, IdentityApi, GrpcServer, GrpcServer.Contracts, or WebClient |
Infrastructure_Repositories_Should_HaveDependencyOnDomain |
Repository implementations in Infrastructure depend on Domain |
GrpcServerContracts_Should_OnlyDependOnDomain |
GrpcServer.Contracts has no dependency on Infrastructure or presentation layers |
WebApi_Should_NotDependOnGrpcServer |
WebApi does not depend on GrpcServer |
IdentityApi_Should_NotDependOnGrpcServer |
IdentityApi does not depend on GrpcServer |
Domain Layer Tests
| Test | Rule |
|---|---|
Domain_Entities_Should_InheritFromBaseEntity |
All non-abstract entities in Domain.Entities inherit from BaseEntity |
Domain_Repositories_Should_BeInterfaces |
All types starting with "I" in Domain.Repositories are interfaces |
Domain_Entities_Should_BeSealed |
All non-abstract entities are sealed |
Infrastructure Layer Tests
| Test | Rule |
|---|---|
Infrastructure_Repositories_Should_ImplementDomainInterfaces |
Repository classes implement IRepository<> |
Infrastructure_DbContext_Should_BeInCorrectNamespace |
DbContext classes reside in Infrastructure.Common.Context |
Naming Convention Tests
| Test | Rule |
|---|---|
WebApi_Dtos_Should_HaveDtoSuffix |
DTOs in WebApi.Common.Dtos end with "Dto" |
GrpcServer_Handlers_Should_HaveHandlerSuffix |
Handler classes end with "Handler" |
GrpcServerContracts_Commands_Should_HaveCommandSuffix |
Command classes end with "Command" |
GrpcServerContracts_Dtos_Should_HaveDtoSuffix |
DTOs in GrpcServer.Contracts end with "Dto" |
WebApi_Endpoints_Should_BeNamedEndpoint |
All endpoint classes are named "Endpoint" |
IdentityApi_Endpoints_Should_BeNamedEndpoint |
All IdentityApi endpoint classes are named "Endpoint" |
Clean Architecture Pattern Tests
| Test | Rule |
|---|---|
Domain_Should_NotDependOnEntityFramework |
Domain has no dependency on Microsoft.EntityFrameworkCore |
Domain_Should_NotDependOnDapper |
Domain has no dependency on Dapper |
Domain_Should_NotDependOnNpgsql |
Domain has no dependency on Npgsql |
Domain_Models_Should_BeRecordsOrClasses |
Models in Domain.Models are classes or sealed |
Domain_Repositories_Should_StartWithI |
Repository interfaces start with "I" |
Infrastructure_Should_NotContainInterfaces |
Only IApplicationDbContext is an acceptable public interface in Infrastructure |
GrpcServer_Handlers_Should_HaveDependencyOnDomain |
gRPC handlers depend on the Domain layer |
Unit Tests (test/WebApi.Unit.Tests/)
Unit tests for WebApi endpoints using NUnit with FakeItEasy for mocking and Shouldly for assertions.
Structure
WebApi.Unit.Tests/
├── Endpoints/
│ └── ... # Tests organized by endpoint
├── Usings.cs
└── WebApi.Unit.Tests.csproj
Key Libraries
| Library | Purpose |
|---|---|
| NUnit | Test framework |
| FakeItEasy | Mocking framework |
| Shouldly | Assertion library |
| FastEndpoints | Endpoint testing support |
| coverlet.collector | Code coverage |
Integration Tests (test/WebApi.Integration.Tests/)
Integration tests that exercise the full request pipeline using FastEndpoints.Testing and xUnit v3.
Structure
WebApi.Integration.Tests/
├── AssemblyInfo.cs
├── Endpoints/
│ └── ... # Integration tests by endpoint
├── Hosts/
│ └── ... # Test host/server configuration
├── Usings.cs
└── WebApi.Integration.Tests.csproj
Key Libraries
| Library | Purpose |
|---|---|
| xUnit v3 | Test framework |
| FastEndpoints.Testing | In-memory test server for FastEndpoints |
| Shouldly | Assertion library |
| Microsoft.NET.Test.Sdk | Test SDK |
Prerequisites
Integration tests require a running PostgreSQL database. In CI, this is provisioned via Docker Compose:
docker compose up db -d --build --force-recreate
sleep 30
Integration tests are currently commented out in CI workflows and are run manually.
Running Tests
Run All Tests
dotnet test cpnucleo.slnx
Run Architecture Tests Only
dotnet test test/Architecture.Tests/
Run Unit Tests Only
dotnet test test/WebApi.Unit.Tests/
Run Integration Tests (requires running database)
# Start the database first
docker compose up db -d
sleep 30
# Run integration tests
dotnet test test/WebApi.Integration.Tests/
Run with Code Coverage
dotnet test --collect:"XPlat Code Coverage"
CI Pipeline Test Execution
Architecture tests run automatically in both CI workflows:
- build-check.yml (PR): runs architecture tests for each service (WebApi, GrpcServer, IdentityApi, WebClient)
- main-release.yml (push to main): runs architecture tests before building Docker images
Unit and integration tests are configured in the workflows but currently commented out, pending further setup.