From 5a6c951641e9b3e4ce9a0d8d810614120406331c Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 22 Oct 2026 04:55:44 +0200 Subject: [PATCH] docs: codify C# functional style, low branching, try-catch avoidance, and Clean/Vertical Slice/SOLID/DRY balance in GEMINI.md --- GEMINI.md | 164 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 108 insertions(+), 56 deletions(-) diff --git a/GEMINI.md b/GEMINI.md index 55a6552..62fcaf4 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -955,64 +955,116 @@ PostFundManagement.Infrastructure PostFundManagement.Domain └── Migrations/ └── Abstractions/ # IEntity, IAggregateRoot, IService ``` -### 16.2 C# 13 & .NET 10 Coding Conventions +### 16.2 Architectural Balance: Clean Architecture, Vertical Slices, SOLID & DRY -1. **Primary Constructors & Modern Types**: - * Services and endpoints use primary constructors: - ```csharp - public sealed class ProjectService( - IDbContextFactory contextFactory, - HashService hashService, - ITenantContextService tenantContext) : IProjectService - ``` - * Extensive use of expression-bodied members (`=>`) for concise method definitions. - * Nullable reference types (`enable`) strictly enforced. -2. **Functional Result Pattern (`FluentResults`)**: - * Domain and Application services **never throw exceptions for predictable business validation failures**. - * Methods return `Task` or `Task>`. - * Standard error objects in `PostFundManagement.Domain.Errors`: - * `ValidationError`: Mandatory field missing, format invalid, or constraint violation. - * `NotFoundError`: Entity ID not found in database. - * `UpdateError`: Mutation failed or produced zero affected rows. - * `CreateError`: Entity creation failed during persistence. - * `IdDecodeError`: HashID token corrupted, invalid salt, or tampering detected. -3. **Unified Exception-to-Result Wrapping**: - * Service methods wrap their implementation inside `Result.Try`: - ```csharp - public async Task> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) => - await Result.Try((Func>>)(async () => - { - // Execution logic... - })); - ``` - * Catches unhandled exceptions and converts them safely into `Result.Fail` without crashing the thread or leaking raw stack traces. -4. **Pattern-Matching Endpoint Handlers**: - * Minimal API endpoints map `Result` directly to HTTP status codes via C# switch expressions: - ```csharp - await service.UpdateProjectStatusAsync(...) switch - { - { IsSuccess: true } => Results.Ok(), - var r when r.HasError() => Results.Forbid(), - var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), - var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), - var r => Results.InternalServerError(r.Errors.ToProblems()) - } - ``` -5. **High-Concurrency DbContext Management**: - * Services inject `IDbContextFactory` rather than scoped `ApplicationDbContext`. - * Creates short-lived contexts using `await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);`. - * Avoids thread concurrency issues in parallel mediator requests and background Quartz jobs. -6. **Query & Mutation Optimizations**: - * Queries use `.AsNoTracking()` to eliminate EF Core tracking overhead. - * Targeted updates leverage `.ExecuteUpdateAsync(...)` for in-database bulk updates without loading full entities into memory. -7. **Tiered Hybrid Caching**: - * Implements `Microsoft.Extensions.Caching.Hybrid` alongside StackExchange Redis. - * Local L1 memory cache: 30-minute expiration. - * Distributed L2 Redis cache: 2-minute expiration (`InstanceName = "PFM-API_"`). +The PFM backend strikes a deliberate, pragmatic balance between **Clean Architecture**, **Vertical Slice Architecture**, **SOLID principles**, and the **DRY (Don't Repeat Yourself)** principle: + +1. **Clean Architecture with Vertical Slices**: + * **Domain Layer**: Completely isolated aggregate roots, entities, taxonomies (`Enums`), domain errors (`IError`), and abstractions. It has no dependencies on database technologies or presentation frameworks. + * **Infrastructure Layer**: Encapsulates EF Core configurations, PostgreSQL mappings, and data protection. + * **Application & Api Layers by Vertical Slice**: Features are organized cleanly into domain feature slices (`Projects`, `Grants`, `Execution`, `Governance`, `Evidence`, `Communications`, `Users`). Each slice groups its commands/queries, records (`Records.cs`), core service (`[Slice]Service`), hybrid cache decorator (`[Slice]CacheService`), and Minimal API endpoints (`[Action]Endpoint.cs`), preserving strong cohesion without code bloat. + +2. **Strict Adherence to SOLID Design Principles**: + * **Single Responsibility Principle (SRP)**: + * `[Slice]Service`: Implements pure business logic, database transactions, and entity mapping. + * `[Slice]CacheService`: Implements read caching and mutation eviction via `HybridCache`. + * `LoggingInterceptor`: Exclusively manages structured logging upon failure. + * `AuditInterceptor`: Exclusively manages regulatory audit logging upon success (`BR-007`). + * `[Action]Endpoint`: Exclusively handles route binding, OpenAPI documentation, and HTTP mapping. + * **Open/Closed Principle (OCP)**: New cross-cutting behaviors (auditing, metrics, tracing) and endpoints are added via decorators, interceptors, and `IEndpoint` implementations without altering existing service logic. + * **Liskov Substitution Principle (LSP)**: `[Slice]CacheService` transparently decorates and substitutes `[Slice]Service` across all consumers via `I[Slice]Service`. + * **Interface Segregation Principle (ISP)**: Lean, dedicated service interfaces per domain slice rather than bloated monolithic service contracts. + * **Dependency Inversion Principle (DIP)**: Endpoints and services depend exclusively on abstractions (`IDbContextFactory`, `ITenantContextService`, `HashService`, service interfaces) rather than concrete implementations. + +3. **DRY Principle via AOP & Functional Utilities**: + * **Zero Cross-Cutting Boilerplate**: Developers never write repetitive logging or auditing code in service methods. Attributes `[Log]` and `[Audit]` handle these concerns declaratively via Castle DynamicProxy. + * **Unified Error & Problem Details**: Domain error records (`ValidationError`, `NotFoundError`, `UpdateError`, `CreateError`, `IdDecodeError`) and extension methods (`ToProblems()`, `ToEndpointName()`) prevent repeated error payload formatting. + * **Shared HashID & Tenant Context**: Centralized `HashService` and `ITenantContextService` guarantee single-point security and tenant isolation enforcement. --- -### 16.3 Logging Strategy Using `LoggingInterceptor` & `[Log]` +### 16.3 C# 13 & .NET 10 Coding Style: Functional Results & Syntax Conventions + +> [!IMPORTANT] +> **Mandatory Assistant Rule (Never Regress or Undo This Approach)**: +> Antigravity / Gemini must strictly adhere to the established functional, low-branching C# style. When adding, modifying, or refactoring backend code: +> - **DO NOT introduce imperative `try-catch` blocks** for control flow or routine domain logic. +> - **DO NOT introduce nested `if-else` return branches**. +> - **DO NOT throw exceptions** for predictable domain validation or mutation failures. +> - **Always use FluentResults** (`Result.Try`, `Result.OkIf`, `Result.FailIf`) and pattern-matching switch expressions on endpoints. + +#### 1. Extensive Avoidance of `try-catch` Blocks +* Imperative `try-catch` blocks are strictly avoided across all application services, endpoints, and domain logic. +* Methods wrap their entire execution functionally inside `Result.Try`: + ```csharp + public async Task> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + // Execution logic without any try-catch... + })); + ``` +* Unexpected runtime exceptions are automatically caught by `Result.Try` and converted into a `Result.Fail` with exception metadata, allowing `LoggingInterceptor` to record errors without leaking raw exceptions or crashing threads. + +#### 2. Minimal Code Branching & Elimination of Nested `if-else` +* Traditional branching constructs (`if-else-if-else`) on returns are eliminated in favor of clean guard clauses and FluentResults combinators: + * **Concise Guard Clauses**: Early validation failure returns: + ```csharp + if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken)) + return Result.Fail(NotFoundError.New); + ``` + * **`Result.OkIf` & `Result.FailIf`**: Replace conditional return statements: + ```csharp + // Mutation success check: + return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled.")); + + // Existence check with mapper transformation: + return Result.FailIf(project is null, NotFoundError.Create("Project not found")) + .ToResult(project?.Map()!); + ``` + * **Chained Functional Evaluations**: Guard against invalid decodes or missing foreign keys with immediate failure returns instead of deeply indented blocks. + +#### 3. Primary Constructors & Expression-Bodied Members +* Services and endpoints use primary constructors to eliminate constructor assignment boilerplate: + ```csharp + public sealed class ProjectService( + IDbContextFactory contextFactory, + HashService hashService, + ITenantContextService tenantContext) : IProjectService + ``` +* Methods leverage expression bodies (`=>`) to delegate directly to `await Result.Try(...)` or endpoint mappings. +* Nullable reference types (`enable`) are strictly enforced. + +#### 4. Pattern-Matching Endpoint Handlers +* Minimal API endpoints map `Result` directly to HTTP status codes via C# switch expressions, completely eliminating endpoint `if-else` branching: + ```csharp + await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + } + ``` + +#### 5. High-Concurrency DbContext Management +* Services inject `IDbContextFactory` rather than scoped `ApplicationDbContext`. +* Creates short-lived contexts using `await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);`. +* Avoids thread concurrency issues in parallel mediator requests and background Quartz jobs. + +#### 6. Query & Mutation Optimizations +* Queries use `.AsNoTracking()` to eliminate EF Core tracking overhead. +* Targeted updates leverage `.ExecuteUpdateAsync(...)` for in-database bulk updates without loading full entities into memory. + +#### 7. Tiered Hybrid Caching +* Implements `Microsoft.Extensions.Caching.Hybrid` alongside StackExchange Redis. +* Local L1 memory cache: 30-minute expiration. +* Distributed L2 Redis cache: 2-minute expiration (`InstanceName = "PFM-API_"`). + +--- + +### 16.4 Logging Strategy Using `LoggingInterceptor` & `[Log]` The application uses Aspect-Oriented Programming (AOP) via **Castle DynamicProxy** and `Castle.Core.AsyncInterceptor` to decouple logging from core domain logic. @@ -1044,7 +1096,7 @@ The application uses Aspect-Oriented Programming (AOP) via **Castle DynamicProxy --- -### 16.4 Audit Strategy Using `AuditInterceptor`, `[Audit]` & `AuditService` +### 16.5 Audit Strategy Using `AuditInterceptor`, `[Audit]` & `AuditService` The audit trail is a strict regulatory requirement (`BR-007`, `NFR-008`, `AT-12`). Rather than writing manual audit code in every handler, the platform automates audit logging via `AuditInterceptor`. @@ -1074,7 +1126,7 @@ The audit trail is a strict regulatory requirement (`BR-007`, `NFR-008`, `AT-12` --- -### 16.5 AOP Service Registration Pipeline +### 16.6 AOP Service Registration Pipeline All application services are registered through a unified decorator-and-proxy composition in [`PostFundManagement.Application/Extensions/ServiceExtensionTools.cs`](file:///c:/Users/Admin/source/repos/PostFundManagement/PostFundManagement.Application/Extensions/ServiceExtensionTools.cs) and [`Services.cs`](file:///c:/Users/Admin/source/repos/PostFundManagement/PostFundManagement.Application/Extensions/Services.cs):