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:
@@ -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):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user