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
This commit is contained in:
638
AGENTS.md
Normal file
638
AGENTS.md
Normal file
@@ -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<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.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<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.
|
||||||
Reference in New Issue
Block a user