cpnucleo
Live application available Try it live →

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

github.com/jonathanperis/cpnucleo

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 Requests with Retry-After: 60 header 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:

  1. Client authenticates via POST /api/login on IdentityApi
  2. Receives JWT token
  3. Includes token in Authorization: Bearer {token} header for WebApi/GrpcServer requests
  4. 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 with Active = true
  • Update(...) -- modifies fields and sets UpdatedAt
  • Remove(...) -- sets Active = false and DeletedAt (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, ExistsAsync
  • IProjectRepository -- specialized repository for Project-specific queries
  • IUnitOfWork -- 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)

  • ApplicationDbContext with IApplicationDbContext interface
  • 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[] via Lazy<> for performance
  • Supports paginated queries with configurable sort column/order and SQL injection protection
  • UnitOfWork wraps NpgsqlConnection + NpgsqlTransaction for transactional operations
  • Dapper.AOT enabled for compile-time SQL interception

Dependency Injection

DependencyInjection.AddInfrastructure() registers:

  • IApplicationDbContext as ApplicationDbContext (EF Core, scoped)
  • NpgsqlConnection (Dapper basic, scoped)
  • IProjectRepository as ProjectRepository (Dapper specialized, scoped)
  • IUnitOfWork as UnitOfWork (Dapper advanced with transactions, scoped)
  • Optional fake data generation via Bogus when CreateFakeData is configured

Presentation Layer

WebApi (src/WebApi)

  • FastEndpoints for REST API with Swagger/OpenAPI documentation
  • Uses EF Core via IApplicationDbContext for 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 IUnitOfWork for 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 BaseEntity and be sealed
  • 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:

  1. Starts PostgreSQL 16.7
  2. Creates the database using credentials from .env
  3. 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 checkpoints
  • max_wal_size=4096 -- larger WAL before checkpoint
  • synchronous_commit=0 -- async commits
  • fsync=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" = true
  • GetAllAsync -- Paginated query with OFFSET/LIMIT, configurable sort column with SQL injection protection
  • AddAsync -- INSERT INTO ... RETURNING "Id" with reflection-based column mapping
  • UpdateAsync -- UPDATE ... SET ... WHERE "Id" = @Id
  • DeleteAsync -- Hard delete (for gRPC operations)
  • ExistsAsync -- SELECT EXISTS(...) check

Performance Optimizations

  • PropertyInfo[] cached via Lazy<> to avoid repeated reflection
  • HashSet<string> for O(1) sort column validation
  • Dapper.AOT for 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:

  1. Bogus generates fake data for all entities
  2. Outputs SQL/CSV dump files
  3. 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: always on 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

  1. base -- mcr.microsoft.com/dotnet/aspnet:10.0 runtime image
  2. build -- mcr.microsoft.com/dotnet/sdk:10.0 with clang/zlib for AOT support; restores, builds
  3. publish -- Publishes with configured optimizations
  4. 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:

  1. 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
  2. Container Healthcheck Test (depends on build)

    • Build Docker image from source
    • Start container via Docker Compose
    • Poll /healthz endpoint 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:

  1. Setup, Build & Test -- Same as build check but with TRIM=true, EXTRA_OPTIMIZE=true, BUILD_CONFIGURATION=Release

  2. Build & Push Docker Image (depends on test)

    • Setup QEMU and Docker Buildx for multi-platform builds
    • Login to GHCR
    • Build and push images for linux/amd64 and linux/arm64/v8
    • Images pushed to GHCR: ghcr.io/jonathanperis/cpnucleo-{service}:latest
  3. Container Healthcheck Test (depends on push)

    • Pull production images
    • Run healthcheck validation
  4. 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:


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.