Appearance
Architecture Overview
Tech Stack
| Layer | Technologies |
|---|---|
| Backend | .NET 10, Minimal APIs (REPR pattern with IEndpoint), EF Core + PostgreSQL 18, FluentValidation |
| Auth | ASP.NET Core native (JWT Bearer, HttpOnly cookies), Axios interceptors + Pinia |
| Cache | FusionCache + Redis |
| Messaging | In-process event dispatch (InProcessEventBus, zero latency, zero DB for integration events) |
| Storage | MinIO (S3-compatible, pinned, never internet-exposed) |
| Analytics | ClickHouse (audit, events) |
| Observability | Serilog, OpenTelemetry, Scalar (API docs) |
| Resilience | Polly (retries, circuit breaker, timeouts) |
| MailKit (SMTP) + React Email + Hono/Bun (renderer) | |
| Frontend | Vue 3 Composition API, Pinia 3, Vue Router, Vite, TypeScript strict |
| UI | shadcn-vue, Tailwind CSS v4, lucide-vue-next |
| Forms | VeeValidate + Zod |
| Lint/Format | oxlint, oxfmt |
| Testing | xUnit v3, Testcontainers (backend) / Vitest, Playwright (frontend) |
Design Patterns
Vertical Slices + Screaming Architecture + Modular Monolith
The backend follows a Vertical Slices architecture where each feature is a self-contained folder with its own Endpoint, Handler, and Validator. Folder names reflect business capabilities, not technical layers. The entire application ships as a single deployable unit with strict module boundaries enforced by architecture tests.
src/
├── Wilo.Api/ Composition root (ASP.NET host, references all modules)
├── Wilo.Common/ Shared kernel (zero project references, pure utilities)
├── Wilo.Contracts/ Inter-module contracts (events, requests, shared DTOs)
├── Wilo.Infrastructure/ Technical cross-cutting concerns (email, storage, outbox, audit)
├── Wilo.Modules.Identity/ Authentication & Identity
├── Wilo.Modules.Members/ Member Management
└── Wilo.Modules.Storage/ File storage endpointsDependency Rules
Wilo.Api
(Composition Root — refs ALL)
/ | \
v v v
Wilo.Modules.* Wilo.Infrastructure
| \ |
v v v
Wilo.Common Wilo.Contracts
|
v
Wilo.Common
(zero refs)| Project | Can reference | CANNOT reference |
|---|---|---|
Wilo.Common | NuGet packages only | Any project |
Wilo.Contracts | Wilo.Common | Modules, Infrastructure, Api |
Wilo.Infrastructure | Wilo.Common, Wilo.Contracts | Modules, Api |
Wilo.Modules.* | Wilo.Common, Wilo.Contracts, Wilo.Infrastructure | Other modules, Api |
Wilo.Api | Everything | — |
Critical rule: No cross-module references. Module A never references Module B. All inter-module communication goes through Wilo.Contracts types.
Feature Slice Pattern (REPR)
Each feature lives in its own folder and contains up to three files:
Wilo.Modules.{Module}/Features/{Operation}/
├── {Op}Endpoint.cs IEndpoint + nested {Op}Request / {Op}Response records
├── {Op}Handler.cs Plain class with HandleAsync (auto-registered by Scrutor)
└── {Op}Validator.cs AbstractValidator<{Op}Endpoint.{Op}Request>Handlers are plain classes — no CQRS interfaces, no MediatR, no decorator chains. Scrutor scans and registers by naming convention (*Handler).
Module Anatomy
Each module exposes a single registration class:
csharp
public static class MembersModuleRegistration
{
// Registers DbContext, handlers, validators, consumers, event handlers in DI
public static WebApplicationBuilder AddMembersModule(this WebApplicationBuilder builder) { ... }
// Maps all IEndpoint implementations from the module's assembly
public static IEndpointRouteBuilder MapMembersEndpoints(this IEndpointRouteBuilder app) { ... }
}Each module has its own PostgreSQL schema and DbContext:
csharp
public sealed class MembersDbContext(DbContextOptions<MembersDbContext> options) : DbContext(options)
{
public const string SchemaName = "members";
public DbSet<Member> Members => Set<Member>();
}Event Dispatch (In-Process)
Integration events are dispatched synchronously in-process via InProcessEventBus. No external message broker, no polling, no DB rows per integration event.
Handler calls db.SaveChangesAsync()
→ OutboxInterceptor (SaveChangesInterceptor):
1. Harvest domain events from ChangeTracker.Entries<Entity>()
2. Dispatch to IDomainEventHandler<T> via DomainEventDispatcher
→ Handlers call InProcessEventBus.PublishAsync() → IEventConsumer<T> (synchronous, in-process)
→ Handlers call auditOutbox.Stage()
3. INSERT audit outbox_messages (same DB transaction — atomic)
→ Transaction committed
→ [Background] AuditOutboxProcessor → ClickHouse (audit pipeline only)Integration events are dispatched with zero latency within the same request scope. The outbox is used exclusively for the audit pipeline.
Inter-Module Communication
Sync (IModuleClient) — when the caller needs an immediate response:
csharp
var result = await moduleClient.SendAsync(new GetMemberByUserIdRequest(userId), ct);Async (IModuleEventBus) — fire-and-forget, dispatched synchronously in-process:
csharp
// Dispatched in-process to all IEventConsumer<T> handlers during SaveChangesAsync
await eventBus.PublishAsync(new MemberSubscribedEvent { ... }, ct);
await db.SaveChangesAsync(ct); // entity changes committed, audit events persisted atomicallyConsumers implement IEventConsumer<T> and are auto-registered by Scrutor.
Infrastructure Services
┌──────────────────────────────────────────────────────┐
│ Wilo.Api │
│ ASP.NET 10 + Minimal APIs │
│ Port 5000 │
└──┬─────┬──────┬──────┬──────┬──────┬──────────────────┘
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
Postgres Redis MinIO Click Email Aspire
:5432 :6379 :9000 House Rend. Dashboard
:8123 :3050 :18888
│
▼
Mailpit
:1025| Service | Purpose | Key Configuration |
|---|---|---|
| PostgreSQL 18 | Main relational database | EF Core + NodaTime + SnakeCaseConvention, one schema per module |
| Redis | Distributed cache | FusionCache L1 (memory) + L2 (Redis) + backplane |
| MinIO | S3-compatible object storage | Buckets: wilo-documents, wilo-images, wilo-temp |
| ClickHouse | Analytical database | Immutable audit_logs table (MergeTree engine) |
| Email Renderer | React Email templates | Hono HTTP server on Bun runtime |
| Mailpit | Dev email testing | SMTP capture on port 1025, Web UI on port 8025 |
| Aspire Dashboard | OpenTelemetry UI | Traces, metrics, logs visualization on port 18888 |
Health Checks
Three health check endpoints follow cloud-native best practices:
| Endpoint | Purpose | External Checks |
|---|---|---|
GET /health | Aggregated status (cached 10s) | All dependencies |
GET /health/live | Liveness probe | None (process responsive only) |
GET /health/ready | Readiness probe (3s timeout each) | PostgreSQL, Redis, MinIO, ClickHouse, Email Renderer |
Response format:
json
{
"status": "Healthy",
"totalDuration": "00:00:01.234",
"entries": {
"postgres": { "status": "Healthy" },
"redis": { "status": "Healthy" }
}
}Prohibited Technologies
| Prohibited | Use Instead |
|---|---|
| BCrypt, Argon2id | PBKDF2-SHA512 (PasswordHasher) |
| Swagger | Scalar |
| FastEndpoints | Minimal APIs (IEndpoint pattern) |
| MVC controllers | Minimal APIs (IEndpoint pattern) |
| MassTransit, RabbitMQ | Outbox Pattern |
| MediatR | Plain handler classes, no CQRS interfaces |
| AutoMapper | Manual mapping or extension methods |
DateTime / DateTimeOffset | NodaTime (Instant, LocalDate) |
Guid.CreateVersion7() | Guid7.NewGuid() wrapper (Medo.Uuid7) |
| ESLint, Prettier, Biome | oxlint, oxfmt |
| Options API (Vue) | Composition API with <script setup> |
| Chart.js | Radix UI |
| Webpack | Vite |
| Razor (email) | React Email |