Files
DocumentService/AGENTS.md
TekH aafe46a738 docs: Update AGENTS.md for Feature 5 (PDF Merge) completion
- Update test count: 82 passed, 7 skipped (was 62 passed)
- Update PdfOperationsController status: Partial (1/N endpoints), 7 tests
- Add PdfOperationsController Status section with merge endpoint details
- Mark merge endpoint as DONE (Phase 1, Priority 5)
- List remaining Phase 2 endpoints: stamp, annotate
2026-07-21 10:22:04 +02:00

24 KiB
Raw Blame History

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:

dotnet build

Run tests (82 passed, 7 skipped as of Feature 5 - PDF Merge):

dotnet test

Run API (Development):

dotnet run --project DocumentOperator.API

Swagger UI: https://localhost:<port>/swagger

Target framework: .NET 8.0
SDK required: 8.0.412 or later (repo has 8.0.41210.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)
  1. 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 Partial (1/N endpoints) 7 (merge endpoint only)
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)

PdfOperationsController Status:

  • POST /api/pdf/operations/merge - DONE (Phase 1, Priority 5) - Merges multiple PDFs with optional page ranges (multipart + Base64)
  • POST /api/pdf/operations/stamp - TODO (Phase 2, Priority 6)
  • POST /api/pdf/operations/annotate - TODO (Phase 2, Priority 6)

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:

var stream = Assembly.GetExecutingAssembly()
    .GetManifestResourceStream("DocumentOperator.Tests.TestData.Pdfs.valid.pdf");

Do NOT commit new binary files without marking them as <EmbeddedResource>.


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<TRequest> 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.

[ApiController]
[Route("api/pdf/validation")]
public class PdfValidationController : ControllerBase
{
    private readonly IMediator _mediator;

    public PdfValidationController(IMediator mediator)
    {
        _mediator = mediator;
    }

    /// <summary>
    /// Validates a PDF document (multipart/form-data)
    /// </summary>
    [HttpPost("validate")]
    [Consumes("multipart/form-data")]
    [ProducesResponseType(typeof(ValidatePdfResponse), 200)]
    [ProducesResponseType(typeof(ProblemDetails), 400)]
    public async Task<IActionResult> 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);
    }

    /// <summary>
    /// Validates a PDF document (Base64 JSON)
    /// </summary>
    [HttpPost("validate")]
    [Consumes("application/json")]
    [ProducesResponseType(typeof(ValidatePdfResponse), 200)]
    [ProducesResponseType(typeof(ProblemDetails), 400)]
    public async Task<IActionResult> 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

public class PdfInputModel
{
    public IFormFile? File { get; set; }
    public string? Base64Pdf { get; set; }
}

[HttpPost("validate")]
public async Task<IActionResult> 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:

public class PdfValidationController(IMediator mediator, ILogger<PdfValidationController> logger) : ControllerBase
{
    // Use parameters directly, no field declarations needed
    public async Task<IActionResult> Validate(...)
    {
        await mediator.Send(...);
    }
}

Wrong:

public class PdfValidationController : ControllerBase
{
    private readonly IMediator _mediator;
    private readonly ILogger<PdfValidationController> _logger;
    
    public PdfValidationController(IMediator mediator, ILogger<PdfValidationController> logger)
    {
        _mediator = mediator;
        _logger = logger;
    }
}

Controller Responsibilities

Controllers should be thin. Do NOT add mapping logic.

Correct:

public async Task<IActionResult> Validate([FromBody] ValidatePdfRequest request, CancellationToken ct)
{
    // Direct pass-through to MediatR
    var result = await mediator.Send(request, ct);
    return Ok(result);
}

Wrong:

public async Task<IActionResult> 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.

public record ValidatePdfRequest
{
    public byte[]? PdfBytes { get; init; }
    public string? Base64Pdf { get; init; }
}

FluentValidation: Ensure exactly ONE is provided:

public class ValidatePdfRequestValidator : AbstractValidator<ValidatePdfRequest>
{
    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:

public async Task<PdfMetadata> 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.

// 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 <specific-files>
  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:

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.