Files
DocumentService/AGENTS.md
TekH f8690b9417 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<T>)

  - 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
2026-07-07 19:00:12 +02:00

639 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<T> 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<T> 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:<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<T> 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 `<EmbeddedResource>`.
---
## 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.**
### 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;
}
/// <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
```csharp
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:**
```csharp
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:**
```csharp
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:**
```csharp
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:**
```csharp
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.**
```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<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:
```csharp
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.
```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 <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:**
```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<T> 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.