docs: codify C# functional style, low branching, try-catch avoidance, and Clean/Vertical Slice/SOLID/DRY balance in GEMINI.md

This commit is contained in:
2026-10-22 04:55:44 +02:00
parent acf8bc224b
commit 5a6c951641
+108 -56
View File
@@ -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<ApplicationDbContext> contextFactory,
HashService hashService,
ITenantContextService tenantContext) : IProjectService
```
* Extensive use of expression-bodied members (`=>`) for concise method definitions.
* Nullable reference types (`<Nullable>enable</Nullable>`) strictly enforced.
2. **Functional Result Pattern (`FluentResults`)**:
* Domain and Application services **never throw exceptions for predictable business validation failures**.
* Methods return `Task<Result>` or `Task<Result<T>>`.
* 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<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Project>>>)(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<T>` directly to HTTP status codes via C# switch expressions:
```csharp
await service.UpdateProjectStatusAsync(...) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
}
```
5. **High-Concurrency DbContext Management**:
* Services inject `IDbContextFactory<ApplicationDbContext>` 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<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Project>>>)(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<ApplicationDbContext> contextFactory,
HashService hashService,
ITenantContextService tenantContext) : IProjectService
```
* Methods leverage expression bodies (`=>`) to delegate directly to `await Result.Try(...)` or endpoint mappings.
* Nullable reference types (`<Nullable>enable</Nullable>`) are strictly enforced.
#### 4. Pattern-Matching Endpoint Handlers
* Minimal API endpoints map `Result<T>` 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<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
}
```
#### 5. High-Concurrency DbContext Management
* Services inject `IDbContextFactory<ApplicationDbContext>` 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):