From f8690b9417c799bbdb795fe34dfaed3d3010ee8c Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 7 Jul 2026 19:00:12 +0200 Subject: [PATCH] docs: Add AGENTS.md architecture guidance for AI agents Comprehensive architecture documentation including: - Clean Architecture with Controller-based API (NOT Minimal API) - Vertical slice architecture pattern - Exception-based error handling (no Result) - Feature-driven development approach - Primary constructor coding standards - Git commit guidelines - Swiss QR Bill backward compatibility decisions Key decisions documented: - Windows-only targeting (no Linux support needed) - Support BOTH multipart/form-data AND Base64 JSON - Separate endpoints for Combined Address (K-Type) legacy support - Multi-tenancy deferred until after all sync features complete --- AGENTS.md | 638 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 638 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3fc6e7b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,638 @@ +# AGENTS.md + +Agent guidance for DocumentOperator service. Read this before working on the codebase. + +--- + +## ⚠️ CRITICAL: Architecture Decision Change + +**Previous developer used Minimal API** (`DocumentEndpoints.cs`), but this is **WRONG**. + +**YOU MUST use Controller-based approach** as specified in `CONTROLLER_ENDPOINTS.md`. + +### Key Differences + +| Previous Approach (WRONG) | Required Approach (CORRECT) | +|---------------------------|------------------------------| +| Minimal API (`DocumentEndpoints.cs`) | **Controllers** (`PdfValidationController`, etc.) | +| Only Base64 JSON | **Both multipart/form-data AND Base64 JSON** | +| `/api/v1/documents/validate` | **`/api/pdf/validation/validate`** | + +**Do NOT follow ROADMAP.md's "Minimal API" guidance.** It conflicts with the requirements. + +### Migration Required + +**Existing code that needs replacement:** +- `DocumentOperator.API/Endpoints/v1/DocumentEndpoints.cs` → Delete, replace with Controllers +- `Program.cs` line 72: `app.MapDocumentEndpoints()` → Replace with `app.MapControllers()` +- `Program.cs` line 44: Add `builder.Services.AddControllers()` +- All DTOs → Support **BOTH** `IFormFile` (multipart) AND `Base64String` (JSON) + +**Dual Input Support Required:** +- Controllers must accept **BOTH** file upload (multipart/form-data) and Base64 JSON +- Each endpoint should have overloads or flexible parameter binding +- Preserve existing Base64 functionality while adding file upload support + +--- + +## Architecture & Development Approach + +**Clean Architecture with Controller-Based API:** +- 4 layers: API → Application → Infrastructure → Domain +- Domain has **ZERO** external dependencies (only standard .NET) +- Feature-driven development: complete one feature end-to-end before starting the next +- Feature = Domain + Infrastructure + Application + **Controller** + Tests + Swagger (all layers) + +**Dependency flow (enforced):** +``` +API → Application → Domain +API → Infrastructure → Application +Infrastructure → Application (for interfaces only) +Domain → NOTHING +``` + +**Vertical Slice structure** (NOT horizontal layers): +``` +Features/Documents/ +├── ValidatePdf/ +│ ├── ValidatePdfQuery.cs (request) +│ ├── ValidatePdfHandler.cs (logic) +│ └── ValidatePdfValidator.cs (validation) +└── ExtractSwissQrCode/ + ├── ExtractSwissQrCodeQuery.cs + ├── ExtractSwissQrCodeHandler.cs + └── ExtractSwissQrCodeValidator.cs +``` + +All files for a feature live together. Do NOT create separate Commands/, Handlers/, Validators/ folders. + +--- + +## 🏗️ Architecture Principles + +### Clean Architecture (Pragmatic) + +**4 Layers with strict dependency rules:** +- API → Application → Domain +- Infrastructure → Application (interfaces only) +- Domain → NOTHING (zero external dependencies) + +**Key Principles:** +- ✅ Testability (Application layer mocks Infrastructure services) +- ✅ Replaceability (swap DevExpress without touching Application) +- ✅ Separation of Concerns +- ❌ NO overengineering (only what we need, YAGNI principle) +- ❌ NO speculative abstractions (wait for 2nd use case) + +### CQRS with MediatR + +**Why MediatR:** +- 1 Command/Query = 1 Handler = 1 Responsibility +- Isolated, testable handlers +- Pipeline Behaviors (Validation, Logging) run centrally +- Avoids bloated services with 20+ methods + +**Pattern:** +- **Command:** Modifies data (ApplyStamp, EmbedCertificate) +- **Query:** Reads data (ValidatePdf returns metadata only) + +**Pipeline:** ValidationBehavior → LoggingBehavior → Handler + +### Vertical Slice Architecture + +**NOT Horizontal** (Commands/, Handlers/, Validators/ folders) +**YES Vertical** (all files for one feature together) + +**Benefits:** +- Related code stays together (high cohesion) +- Easier to find ("Where's ValidatePdf?" → one folder!) +- Easier to modify (all files in same folder) +- Fewer merge conflicts in teams + +### Exception-Based Error Handling + +**NO Result pattern library** + +**Flow:** +1. FluentValidation (DTO level) → ValidationException → 400 +2. Domain validation → DomainValidationException → 400 +3. Business logic → DomainException → 400/404 +4. Infrastructure → PdfProcessingException → 500 + +**Middleware:** Central exception handler maps exceptions to HTTP status codes + +**Why exceptions:** +- Simpler code (no `if (result.IsSuccess)` everywhere) +- Less boilerplate (no Result wrapping) +- Standard .NET exception flow +- Centralized error handling (one place to maintain) + +### Feature-Driven Development + +**Feature-Driven (NOT Layer-by-Layer):** +- Complete one feature end-to-end before starting next +- Feature = Domain + Infrastructure + Application + API + Tests + Swagger +- Feature is DONE when testable in Swagger UI + +**Why:** +- Faster value delivery (Feature 1 done in ~1 day) +- Clear definition of done (Swagger testable) +- Less complexity (not all layers in parallel) +- Better learning (pattern repeats) + +**Alternative rejected:** Complete all Domain → all Infrastructure → all Application → all API +**Problem:** Too much speculative code without visible results + +### Test-Driven Development (TDD) + +**Flow:** Red → Green → Refactor + +**Test Pyramid:** +- **Unit Tests (many):** Value Objects, Handlers, Services +- **Integration Tests (some):** Endpoints, MediatR Pipeline +- **E2E Tests (few/none):** API is already top-level + +**Why TDD:** +- Tests as documentation +- Tests as safety net for refactoring +- Better design (testable = good code) +- No forgotten tests (test comes FIRST) + +### Cross-Cutting Concerns Timing + +**Multi-Tenancy Implementation Deferred** + +**Decision:** Implement multi-tenancy (X-API-Key header, tenant database, Redis cache) AFTER all synchronous PDF operation features are complete. + +**Why:** +- Multi-tenancy affects ALL endpoints +- Better to implement once for all features (avoid repetition) +- Easier to test features first without tenancy, then add tenancy layer +- Cleaner separation: Features first, then cross-cutting concerns + +**Impact on current architecture:** +- ❌ NO Entity Framework yet (tenant database comes with multi-tenancy) +- ❌ NO Redis yet (API key caching comes with multi-tenancy) +- ❌ NO X-API-Key authentication yet (comes with multi-tenancy) +- ✅ All features currently work without authentication + +**When to implement:** +After completing all Phase 1-3 controllers (PdfValidation, PdfAttachment, PdfOperations, PdfRender, PdfConversion), then add multi-tenancy to ALL endpoints in one refactoring phase. + +--- + +## Build, Test, Run + +**Build:** +```powershell +dotnet build +``` + +**Run tests (19 tests as of Feature 2):** +```powershell +dotnet test +``` + +**Run API (Development):** +```powershell +dotnet run --project DocumentOperator.API +``` +Swagger UI: `https://localhost:/swagger` + +**Target framework:** .NET 8.0 +**SDK required:** 8.0.412 or later (repo has 8.0.412–10.0.203 available) + +--- + +## Key Libraries & Their Roles + +| Library | Purpose | Where Used | +|---------|---------|------------| +| **DevExpress.Document.Processor** (26.1.3) | PDF operations (validation, QR extraction, attachments) | Infrastructure layer only | +| **Codecrete.SwissQRBill.Generator** (3.4.0) | Swiss QR Bill parsing (Standard 2.0) | Infrastructure.Services.QrCodeProcessing | +| **ZXing.Net.Bindings.Windows.Compatibility** (0.16.14) | QR code image decoding | Infrastructure.Services.QrCodeProcessing | +| **MediatR** (14.1.0) | CQRS: 1 handler per feature | Application layer | +| **FluentValidation** (12.1.1) | Request validation (runs via ValidationBehavior before handlers) | Application layer | +| **Serilog.AspNetCore** (10.0.0) | Structured logging | API layer | + +**Critical:** DevExpress requires a license. All PDF operations use `DevExpress.Pdf.PdfDocumentProcessor`. + +--- + +## Exception Handling Strategy + +**No Result pattern.** Use exceptions + central middleware. + +**Flow:** +1. FluentValidation validates request DTOs → throws `ValidationException` → HTTP 400 +2. Domain validation in Value Objects → throws `DomainValidationException` → HTTP 400 +3. Business logic errors → throws `DomainException` subtypes → HTTP 400/404/500 +4. Infrastructure errors (e.g., PDF parsing) → throws `PdfProcessingException` → HTTP 500 + +**Middleware maps exceptions to HTTP status codes** (`ExceptionHandlingMiddleware.cs`). + +Do NOT add `if (result.IsSuccess)` checks. Throw exceptions for errors. The middleware handles the rest. + +--- + +## Required Controllers & Endpoints + +**See `CONTROLLER_ENDPOINTS.md` for complete specification.** + +### Priority Order + +**Phase 1 (PRIORITY):** +1. `PdfValidationController` – 2 endpoints + - `POST /api/pdf/validation/validate` (Basic PDF validation) + - `POST /api/pdf/validation/validate-pdfa` (PDF/A conformance) +2. `PdfAttachmentController` – 2 endpoints + - `POST /api/pdf/attachments/check` (Attachment detection) + - `POST /api/pdf/attachments/extract` (Extract attachments) +3. `PdfOperationsController` – Merge endpoint + - `POST /api/pdf/operations/merge` (Merge multiple PDFs) + +**Phase 2:** +4. `PdfOperationsController` – Stamp & Annotate +5. `PdfRenderController` – Preview + +**Phase 3:** +6. `PdfConversionController` – PDF ↔ PDF/A conversion + +### Current Status + +| Controller | Status | Tests | +|-----------|--------|-------| +| **PdfValidationController** | ⏳ Pending | 0 | +| **PdfAttachmentController** | ⏳ Pending | 0 | +| **PdfOperationsController** | ⏳ Pending | 0 | +| **PdfRenderController** | ⏳ Pending | 0 | +| **PdfConversionController** | ⏳ Pending | 0 | + +**Legacy code exists** (`DocumentEndpoints.cs` with Minimal API + Base64 JSON), but **must be replaced** with Controllers + multipart/form-data. + +--- + +## Adding a New Feature + +**Required steps (follow CONTROLLER_ENDPOINTS.md):** + +1. **Domain:** Value Objects, Exceptions (if needed) +2. **Infrastructure:** Service interface + DevExpress implementation + unit tests +3. **Application:** Query/Command + Handler + FluentValidator + DTOs + unit tests +4. **API:** **Controller** + actions + integration tests +5. **Swagger:** XML comments on controller actions + DTOs + +**Example (PdfValidationController):** +``` +Step 1: Application/Features/Documents/ValidatePdf/ + - ValidatePdfCommand.cs (record) + - ValidatePdfHandler.cs (IRequestHandler) + - ValidatePdfValidator.cs (AbstractValidator) +Step 2: API/Controllers/PdfValidationController.cs + - [HttpPost("validate")] action + - Accepts IFormFile (multipart/form-data) + - Returns ValidatePdfResponse +Step 3: XML comments + [ProducesResponseType] attributes +``` + +**CRITICAL: Support BOTH multipart/form-data AND Base64 JSON for all file-based endpoints.** + +**Input Flexibility:** +- Primary: `IFormFile` (multipart/form-data) - for direct file uploads +- Secondary: `Base64String` (application/json) - for API clients that can't send multipart + +Do NOT skip steps. Each feature is done when it's **testable in Swagger UI with both input methods**. + +--- + +## Test Data + +**Embedded test PDFs:** +- `TestData/Pdfs/valid.pdf` (simple PDF for validation) +- `TestData/Pdfs/pdfWithSwissQRCode.pdf` (Swiss QR Code on last page) +- `TestData/Pdfs/pdfWithMoreThanOneAttachment.pdf` (6 attachments) + +**All test PDFs are EmbeddedResource.** Access via: +```csharp +var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("DocumentOperator.Tests.TestData.Pdfs.valid.pdf"); +``` + +**Do NOT commit new binary files** without marking them as ``. + +--- + +## Swiss QR Code Feature (Feature 2) + +**Swiss QR Bill Standard 2.0** requires: +- QR code is on the **last page** of the PDF (not first!) +- Use `DevExpress.Pdf.PdfDocumentProcessor` to render last page as image +- Use `ZXing` to decode QR code from image +- Use `Codecrete.SwissQRBill.Generator` to parse Swiss QR Bill payload + +**Known quirks:** +- PDF must be rendered at **300 DPI** for reliable QR detection +- Alternative procedure parameters (AV1, AV2) are split by newline, not semicolon + +--- + +## MediatR Pipeline Behaviors + +**Two behaviors run for EVERY request:** + +1. **ValidationBehavior** (runs first): Executes all `IValidator` and throws `ValidationException` if invalid +2. **LoggingBehavior** (runs second): Logs request name + execution time + +**Registered in:** `Application/DependencyInjection.cs` + +Do NOT manually call validators in handlers. The pipeline does it. + +--- + +## Controller Pattern (CORRECT Approach) + +**Controllers must support BOTH file upload and Base64 input.** + +### Option 1: Separate Endpoints (Recommended) + +```csharp +[ApiController] +[Route("api/pdf/validation")] +public class PdfValidationController : ControllerBase +{ + private readonly IMediator _mediator; + + public PdfValidationController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// Validates a PDF document (multipart/form-data) + /// + [HttpPost("validate")] + [Consumes("multipart/form-data")] + [ProducesResponseType(typeof(ValidatePdfResponse), 200)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + public async Task ValidateFromFile(IFormFile file, CancellationToken ct) + { + using var ms = new MemoryStream(); + await file.CopyToAsync(ms, ct); + byte[] pdfBytes = ms.ToArray(); + + var command = new ValidatePdfCommand(pdfBytes); + var result = await _mediator.Send(command, ct); + + return Ok(result); + } + + /// + /// Validates a PDF document (Base64 JSON) + /// + [HttpPost("validate")] + [Consumes("application/json")] + [ProducesResponseType(typeof(ValidatePdfResponse), 200)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + public async Task ValidateFromBase64( + [FromBody] ValidatePdfRequest request, + CancellationToken ct) + { + var query = new ValidatePdfQuery(Base64String.Create(request.Base64Pdf)); + var result = await _mediator.Send(query, ct); + + return Ok(result); + } +} +``` + +### Option 2: Single Endpoint with Model Binding + +```csharp +public class PdfInputModel +{ + public IFormFile? File { get; set; } + public string? Base64Pdf { get; set; } +} + +[HttpPost("validate")] +public async Task Validate([FromForm] PdfInputModel input, CancellationToken ct) +{ + byte[] pdfBytes = input.File != null + ? await GetBytesFromFile(input.File) + : Base64String.Create(input.Base64Pdf!).ToByteArray(); + + // Process... +} +``` + +**Use Controllers, NOT Minimal API endpoints.** + +--- + +## Configuration + +**appsettings.json sections:** +- `DocumentOperatorSettings` (future: file size limits, temp paths) +- `RedisSettings` (future: multi-tenancy caching) +- `ApiKeySettings` (future: authentication) + +**Currently:** All features work without authentication. Multi-tenancy is deferred until after all sync features are complete. + +--- + +## Coding Standards + +### Primary Constructors +**ALWAYS use primary constructors** (C# 12 feature) unless there's a technical limitation. + +**✅ Correct:** +```csharp +public class PdfValidationController(IMediator mediator, ILogger logger) : ControllerBase +{ + // Use parameters directly, no field declarations needed + public async Task Validate(...) + { + await mediator.Send(...); + } +} +``` + +**❌ Wrong:** +```csharp +public class PdfValidationController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public PdfValidationController(IMediator mediator, ILogger logger) + { + _mediator = mediator; + _logger = logger; + } +} +``` + +### Controller Responsibilities +**Controllers should be thin.** Do NOT add mapping logic. + +**✅ Correct:** +```csharp +public async Task Validate([FromBody] ValidatePdfRequest request, CancellationToken ct) +{ + // Direct pass-through to MediatR + var result = await mediator.Send(request, ct); + return Ok(result); +} +``` + +**❌ Wrong:** +```csharp +public async Task Validate([FromBody] ValidatePdfRequest request, CancellationToken ct) +{ + // Manual mapping (WRONG!) + var command = new ValidatePdfCommand(request.Base64Pdf); + var metadata = await mediator.Send(command, ct); + var response = new ValidatePdfResponse(metadata.PageCount, ...); + return Ok(response); +} +``` + +**If mapping is absolutely necessary:** Use AutoMapper. + +### Request DTOs - Flexible Input +**Support BOTH `byte[]` and `Base64String` in requests.** + +```csharp +public record ValidatePdfRequest +{ + public byte[]? PdfBytes { get; init; } + public string? Base64Pdf { get; init; } +} +``` + +**FluentValidation:** Ensure exactly ONE is provided: +```csharp +public class ValidatePdfRequestValidator : AbstractValidator +{ + public ValidatePdfRequestValidator() + { + RuleFor(x => x) + .Must(x => (x.PdfBytes != null && x.PdfBytes.Length > 0) ^ + (!string.IsNullOrWhiteSpace(x.Base64Pdf))) + .WithMessage("Either PdfBytes or Base64Pdf must be provided, but not both"); + } +} +``` + +**Handler:** Use `byte[]` if available, otherwise convert Base64: +```csharp +public async Task Handle(ValidatePdfRequest request, CancellationToken ct) +{ + byte[] pdfBytes = request.PdfBytes ?? Convert.FromBase64String(request.Base64Pdf!); + return await _processor.ValidateAsync(pdfBytes); +} +``` + +### No Unnecessary Value Objects +**Do NOT create value objects for simple types** (e.g., Base64String). + +**❌ Wrong:** Creating `Base64String` value object just to wrap `string` +**✅ Correct:** Use `string` directly + extension methods if needed + +**Why:** +- Performance overhead (validation runs twice: once in value object, once in FluentValidation) +- Unnecessary abstraction (YAGNI principle) +- `Convert.FromBase64String()` already validates format + +### Exception Handling in Controllers +**Let FormatException bubble up naturally.** ExceptionHandlingMiddleware will catch it. + +```csharp +// Handler +byte[] pdfBytes = request.PdfBytes ?? Convert.FromBase64String(request.Base64Pdf!); +// If Base64 is invalid, FormatException → Middleware → 400 Bad Request +``` + +**Middleware handles:** +- `FormatException` → 400 Bad Request +- `ValidationException` → 400 Bad Request +- `DomainException` → 400/404 +- `PdfProcessingException` → 500 + +--- + +## Git Commit Guidelines + +### ⚠️ CRITICAL: Never Commit Without Approval +**NEVER run `git commit` without explicit user approval.** + +### Systematic Commits +**Do NOT commit everything in one giant commit.** + +**✅ Correct approach:** +1. Complete one logical change (e.g., "Add PdfValidationController") +2. Stage only related files: `git add ` +3. Ask user: "Ready to commit 'Add PdfValidationController'?" +4. After approval: `git commit -m "Add PdfValidationController with dual input support"` +5. Repeat for next logical change + +**❌ Wrong approach:** +```bash +git add -A +git commit -m "Migrate everything to controllers, update tests, add AGENTS.md, delete ROADMAP.md" +# This is TOO MUCH in one commit! +``` + +**Good commit messages:** +- `feat: Add PdfValidationController with multipart/form-data support` +- `refactor: Replace Base64String value object with direct string usage` +- `test: Add integration tests for PdfValidationController` +- `docs: Add AGENTS.md with architecture guidance` +- `chore: Delete deprecated ROADMAP.md` + +**Commit size guideline:** 1-5 files per commit, one logical change + +--- + +## What NOT to Do + +- ❌ Do NOT create horizontal folders (Commands/, Handlers/, Validators/) +- ❌ Do NOT add Entity Framework until multi-tenancy phase +- ❌ Do NOT use Minimal API endpoints (use Controllers instead) +- ❌ Do NOT support only ONE input type (must support BOTH multipart AND Base64) +- ❌ Do NOT use Result pattern (use exceptions) +- ❌ Do NOT skip tests (TDD: write test first, then implementation) +- ❌ Do NOT add dependencies to Domain layer (keep it clean!) +- ❌ Do NOT commit without user approval +- ❌ Do NOT use old-style constructors (use primary constructors) +- ❌ Do NOT add mapping logic in controllers (keep them thin) +- ❌ Do NOT create unnecessary value objects (YAGNI principle) + +--- + +## Debugging Tips + +**DevExpress PDF errors:** +- Check if file is actually a valid PDF (magic bytes: `%PDF-`) +- DevExpress throws generic exceptions; wrap in try-catch and add context + +**Swiss QR Code not found:** +- Verify QR is on **last page** (not first) +- Check DPI setting (300 DPI required, see ROADMAP.md Feature 2) +- Use `ZXing` with `TryHarder` hint enabled + +**Attachment count wrong:** +- Search entire PDF stream, not just first 1000 chars (see ROADMAP.md fix log 17.01.2025) +- Count `/EmbeddedFiles` object references correctly (not divided by 2) + +--- + +## References + +- **CONTROLLER_ENDPOINTS.md** – **PRIMARY SOURCE** for API specification (all planned endpoints) +- **REQUIRED_FEATURES.md** – Business requirements (what PDF operations are needed and why) +- **ROADMAP.md** – Feature-by-feature implementation plan (1101 lines, detailed) **NOTE: Uses Minimal API, which is incorrect. Follow CONTROLLER_ENDPOINTS.md instead.** +- **DevExpress Docs** – https://docs.devexpress.com/OfficeFileAPI/ +- **Swiss QR Bill Standard** – https://www.ferd-net.de/ (ZUGFeRD/XRechnung context) + +**When implementing endpoints:** Follow CONTROLLER_ENDPOINTS.md, NOT ROADMAP.md's Minimal API approach.