# 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, SwissQrCode, PdfOperations, PdfConversion), then add multi-tenancy to ALL endpoints in one refactoring phase. --- ## Build, Test, Run **Build:** ```powershell dotnet build ``` **Run tests (62 passed, 7 skipped as of Feature 4 - PDF Attachment Extraction):** ```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` – check endpoint - `POST /api/pdf/attachments/check` (Attachment detection) 3. `SwissQrCodeController` – extract endpoint - `POST /api/swissqrcode/extract` (Swiss QR Bill extraction) 4. `PdfAttachmentController` – extract endpoint - `POST /api/pdf/attachments/extract` (Extract attachments as ZIP) 5. `PdfOperationsController` – merge endpoint - `POST /api/pdf/operations/merge` (Merge multiple PDFs) **Phase 2:** 6. `PdfOperationsController` – stamp & annotate - `POST /api/pdf/operations/stamp` (Add stamps) - `POST /api/pdf/operations/annotate` (Add annotations) 7. `PdfAttachmentController` – add attachment - `POST /api/pdf/attachments/add` (Embed attachments in PDF/A-3) **Phase 3:** 8. `PdfConversionController` – PDF ↔ PDF/A conversion - `POST /api/pdf/conversion/to-pdfa` (Convert to PDF/A) - `POST /api/pdf/conversion/from-pdfa` (Convert from PDF/A) **Removed:** - `PdfRenderController` – Moved to .NET client library (WinForms/WPF DevExpress controls) ### Current Status | Controller | Status | Tests | |-----------|--------|-------| | **PdfValidationController** | ✅ DONE | 13 (7 validate + 6 validate-pdfa) | | **SwissQrCodeController** | ✅ DONE | 2 | | **PdfAttachmentController** | ⏳ Partial (2/3 endpoints) | 10 (4 check + 6 extract) | | **PdfOperationsController** | ⏳ Pending | 0 | | **PdfConversionController** | ⏳ Pending | 0 | **PdfAttachmentController Status:** - ✅ `POST /api/pdf/attachments/check` - DONE (with multipart + Base64 support) - ✅ `POST /api/pdf/attachments/extract` - DONE (Phase 1, Priority 4) - Returns ZIP with all attachments - ⏳ `POST /api/pdf/attachments/add` - TODO (Phase 2, Priority 7) **Note:** PdfRenderController removed - moved to .NET client library. --- ## 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 ``. --- ## Test Structure & Strategy **3-folder structure (CORRECT approach by previous developer):** ``` DocumentOperator.Tests/ ├── Integration/ │ └── API/ │ ├── PdfValidationControllerTests.cs (13 tests) │ └── ExtractSwissQrCodeEndpointTests.cs (2 tests) ├── TestData/ │ └── Pdfs/ (EmbeddedResource PDFs) ├── Unit/ │ ├── Application/ │ │ └── Features/ │ │ ├── ValidatePdf/ │ │ │ └── ValidatePdfHandlerTests.cs (2 tests) │ │ ├── ValidatePdfA/ │ │ │ └── ValidatePdfAQueryHandlerTests.cs (4 tests) │ │ └── ExtractSwissQrCode/ │ │ └── ExtractSwissQrCodeHandlerTests.cs (2 tests) │ └── Infrastructure/ │ └── Services/ │ └── PdfProcessing/ │ └── DevExpressPdfProcessorTests.cs (7 tests) ``` **✅ Why this structure is CORRECT:** 1. **Integration vs Unit separation:** - **Integration:** WebApplicationFactory → REAL API calls (HTTP, middleware, MediatR pipeline, DevExpress) - **Unit:** Mock-based ISOLATED tests (Handler only depends on mocked IPdfProcessor) 2. **TestData centralization:** - All 3 layers share same EmbeddedResource PDFs (no duplication) - Accessed via `Assembly.GetManifestResourceStream()` 3. **Vertical Slice compliance:** - `Unit/Application/Features/ValidatePdf/` → Each feature's tests co-located - Matches Application layer structure exactly 4. **Test Pyramid:** - **Unit tests (15):** Fast, isolated, many scenarios - **Integration tests (15):** Slower, full pipeline, critical paths only **Test count:** 30 tests total (as of Feature 3 - PDF/A Validation) **FluentValidation in tests:** - Base64 format validation happens in `ValidatePdfQueryValidator` and `ValidatePdfAQueryValidator` - Prevents `FormatException` from reaching handler (caught as 400 Bad Request, not 500) - Unit tests verify handler behavior with valid inputs only - Integration tests verify full validation pipeline (including FluentValidation) --- ## 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.