# 📘 DocumentOperator - Project Roadmap > **Last Updated:** 16.06.2026 | **Status:** In Development | **Phase:** 2 (Domain Layer) --- ## 📋 TABLE OF CONTENTS 1. [Project Overview](#project-overview) 2. [Architecture & Design Decisions](#architecture--design-decisions) 3. [Technology Stack](#technology-stack) 4. [Project Structure](#project-structure) 5. [Development Roadmap](#development-roadmap) 6. [Current Status](#current-status) --- ## 🎯 PROJECT OVERVIEW ### Vision & Purpose **DocumentOperator** ist ein zentralisierter REST API Service für PDF-Dokumenten-Operationen in einer Multi-Tenant DMS-Umgebung. ### Problem Statement **Aktuell:** - Verschiedene DMS-Kunden bei unterschiedlichen Mandanten - Jede Anwendung implementiert PDF-Operationen redundant - Keine zentrale Stelle für Dokumenten-Verarbeitung - Wartungsaufwand multipliziert sich mit jeder Anwendung **Lösung:** - **Ein** zentraler Service für alle PDF-Operationen - Wiederverwendbar über HTTP REST API - Mandantenfähig (Multi-Tenancy) - Wartbar an einer Stelle --- ### Core Features Der Service bietet folgende PDF-Operationen: #### 1. **PDF Validierung** - Prüfung auf gültiges PDF-Format - Korruptions-Erkennung - Metadaten-Extraktion (Seitenzahl, Größe, Version) #### 2. **Attachment-Extraktion** - Erkennung von eingebetteten Anhängen - Extraktion in temporären Ordner - Rückgabe als Base64 oder Download-Link #### 3. **PDF-Konkatenation** - Zusammenführen mehrerer PDFs - Reihenfolge konfigurierbar - Seitenzahl-Optimierung #### 4. **Stempel/Wasserzeichen** - Aufbringen von Stamps (Logo, Text) - Positions-Konfiguration - Mandanten-spezifische Logos #### 5. **Zertifikat-Einbettung** - PFX-Zertifikate als Attachment einbetten - Digitale Signatur-Vorbereitung - Workflow-Integration (Ergebnisbericht → Zertifikat → Siegel) --- ### Business Workflow ``` Client Application ↓ [HTTP Request] - JSON mit Base64-PDF ↓ DocumentOperator API ↓ [Validierung] → [Operation(en)] → [Ergebnis] ↓ [HTTP Response] - JSON mit verarbeitetem PDF (Base64) ``` **Typischer Ablauf:** 1. Client sendet PDF als Base64 in JSON 2. API validiert Input (FluentValidation) 3. PDF wird in Byte-Array konvertiert 4. Operationen werden durchgeführt (DevExpress) 5. Temporäre Dateien werden erstellt/bereinigt 6. Ergebnis wird als Base64 zurückgegeben --- ## 🏛️ ARCHITECTURE & DESIGN DECISIONS ### Clean Architecture Wir verwenden **Clean Architecture** mit 4 Layers: ``` ┌─────────────────────────────────────┐ │ API Layer (Endpoints) │ ← HTTP Entry Point ├─────────────────────────────────────┤ │ Application Layer (Use Cases) │ ← Business Logic Orchestration ├─────────────────────────────────────┤ │ Infrastructure Layer (Tech Stack) │ ← DevExpress, File I/O, Redis ├─────────────────────────────────────┤ │ Domain Layer (Core Logic) │ ← Business Rules, Models └─────────────────────────────────────┘ ``` #### Dependency Rule (Kritisch!) **Abhängigkeiten zeigen immer nach innen:** ``` API → Application → Domain API → Infrastructure → Domain Infrastructure → Application Domain → NICHTS! (No External Dependencies) Application → NUR Domain ``` **Warum?** - Domain = reine Geschäftslogik, technologie-unabhängig - Application = Use Cases, kennt nur Interfaces - Infrastructure = technische Details, austauschbar - API = dünne Schicht, nur Routing --- ### CQRS with MediatR **Pattern:** Command Query Responsibility Segregation **Warum MediatR?** - ✅ Klare Trennung: 1 Command/Query = 1 Handler - ✅ Single Responsibility Principle - ✅ Pipeline Behaviors (Validation, Logging, etc.) - ✅ Bessere Testbarkeit - ✅ Keine aufgeblähten Service-Klassen **Statt:** ```csharp public class DocumentService { public void Process() { } public void Validate() { } public void Extract() { } // ... 20 Methoden } ``` **Nutzen wir:** ```csharp // Feature: ProcessDocument public class ProcessDocumentCommand : IRequest { } public class ProcessDocumentHandler : IRequestHandler { } public class ProcessDocumentValidator : AbstractValidator { } ``` --- ### Vertical Slice Architecture **Statt Horizontal Layers** (Commands/, Handlers/, Validators/): **Nutzen wir Vertical Slices** (pro Feature alles zusammen): ``` Features/ ├── ProcessDocument/ │ ├── ProcessDocumentCommand.cs │ ├── ProcessDocumentHandler.cs │ └── ProcessDocumentValidator.cs ├── ExtractAttachments/ │ ├── ExtractAttachmentsCommand.cs │ ├── ExtractAttachmentsHandler.cs │ └── ExtractAttachmentsValidator.cs ``` **Vorteile:** - ✅ Zusammengehöriger Code ist zusammen - ✅ Einfacher zu finden und zu ändern - ✅ Feature-basierte Organisation (nicht technische Schichten) - ✅ Besser für Teams (weniger Merge-Konflikte) --- ### Exception-based Error Handling (Zentral!) **Entscheidung:** Keine Result Pattern Library (Ardalis.Result entfernt) **Stattdessen:** 1. **Domain Exceptions** für fachliche Fehler 2. **FluentValidation** für Input-Validierung 3. **Zentrale Exception Handling Middleware** im API Layer **Warum Exception-basiert?** - ✅ Einfacherer Code (kein `if (result.IsSuccess)` überall) - ✅ Weniger Boilerplate - ✅ Ein Package weniger (keine Extra-Lib) - ✅ **Zentrales Error Handling** = bessere Wartbarkeit - ✅ Standard .NET Exception-Flow **Flow:** ``` Request → Validation (FluentValidation Behavior) ↓ Handler (wirft Exception bei Fehler) ↓ Middleware (fängt Exception, mappt zu HTTP Code) ↓ Response (Problem Details RFC 7807) ``` --- ### Minimal APIs (statt Controllers) **Warum Minimal APIs?** - ✅ .NET 8 Best Practice - ✅ Weniger Boilerplate (keine Controller-Klassen) - ✅ Direkte Endpoint-Definition - ✅ Swagger funktioniert 1:1 - ✅ Bessere Performance - ✅ Moderner, funktionaler Stil **Beispiel:** ```csharp app.MapPost("/api/v1/documents/process", async ( ProcessDocumentRequest request, IMediator mediator) => { var command = new ProcessDocumentCommand(request); var result = await mediator.Send(command); return Results.Ok(result); }) .WithName("ProcessDocument") .WithOpenApi(); ``` --- ### Multi-Tenancy via API-Keys **Konzept:** - Jeder Mandant (Customer A, B, C...) hat eigenen API-Key - API-Key wird in HTTP Header gesendet: `X-API-Key: customer-a-key-12345` - Middleware resolved API-Key → Tenant-Context - Tenant-spezifische Einstellungen (Logo für Stamps, Zertifikat, etc.) **Warum API-Keys?** - ✅ Einfach für Service-to-Service Communication - ✅ Security + Tenant-Identification kombiniert - ✅ Swagger-kompatibel (für BB-Tests) - ✅ Einfaches Rate-Limiting pro Tenant **Flow:** ``` Request mit Header "X-API-Key: abc123" ↓ TenantResolutionMiddleware ↓ API-Key → Tenant-Konfiguration ↓ ITenantContext (Scoped DI) ↓ Handler nutzt Tenant-Settings ``` --- ## 🛠️ TECHNOLOGY STACK ### Core Framework | Technology | Version | Purpose | |------------|---------|---------| | **.NET** | 8.0 | Runtime & Framework | | **ASP.NET Core** | 8.0 | Web API | | **C#** | 12 | Language (mit Primary Constructors, Record Types) | --- ### Key Libraries & Packages #### API Layer | Package | Version | Purpose | |---------|---------|---------| | **Swashbuckle.AspNetCore** | 6.6.2 | Swagger/OpenAPI Documentation | | **Serilog.AspNetCore** | 10.0.0 | Strukturiertes Logging | | **Serilog.Sinks.File** | 7.0.0 | Log-Datei-Output | | **Serilog.Enrichers.Environment** | 3.0.1 | Log-Enrichment (MachineName, etc.) | | **Asp.Versioning.Http** | 8.1.1 | API Versioning (/api/v1/, /api/v2/) | | **Microsoft.Extensions.Caching.StackExchangeRedis** | 8.0.28 | Redis Distributed Cache | #### Application Layer | Package | Version | Purpose | |---------|---------|---------| | **MediatR** | 14.1.0 | CQRS Pattern Implementation | | **FluentValidation** | 12.1.1 | Input Validation | | **FluentValidation.DependencyInjectionExtensions** | 12.1.1 | DI Integration | | ~~Ardalis.Result~~ | ~~10.1.0~~ | ❌ **ENTFERNT** (Exception-basiert stattdessen) | #### Infrastructure Layer | Package | Version | Purpose | |---------|---------|---------| | **DevExpress.Pdf.Core** | 25.2.8 | PDF-Operationen (Merge, Extract, Sign, etc.) | | **Microsoft.Extensions.Options.ConfigurationExtensions** | 8.0.0 | Options Pattern | #### Domain Layer | Package | Version | Purpose | |---------|---------|---------| | - | - | **Keine Dependencies!** (Clean Architecture) | --- ### Infrastructure Components | Component | Technology | Purpose | |-----------|------------|---------| | **Hosting** | IIS | Production Deployment | | **Cache** | Redis | Distributed Cache (API-Keys, Tenant-Settings) | | **Message Queue** | (Future) RabbitMQ/Azure Service Bus | Async Processing für große PDFs | | **Logging** | Serilog → File/Console | Strukturiertes Logging | | **Temp Storage** | Local File System | Temporäre PDF-Dateien (später: Blob Storage) | --- ## 📁 PROJECT STRUCTURE ### Solution Overview ``` DocumentOperator/ ├── DocumentOperator.API/ ← HTTP Entry Point ├── DocumentOperator.Application/ ← Use Cases (MediatR Handlers) ├── DocumentOperator.Infrastructure/← Technical Implementations ├── DocumentOperator.Domain/ ← Core Business Logic └── ROADMAP.md ← This file ``` --- ### 🌐 API Layer (DocumentOperator.API) **Purpose:** HTTP Entry Point, Routing, Middleware **References:** - → Application - → Infrastructure - → Domain **NuGet Packages:** - Swashbuckle.AspNetCore (Swagger) - Serilog.AspNetCore + Sinks - Asp.Versioning.Http - Microsoft.Extensions.Caching.StackExchangeRedis **Folder Structure:** ``` DocumentOperator.API/ ├── Endpoints/ │ └── v1/ │ └── DocumentEndpoints.cs ← Minimal API Endpoints ├── Middleware/ │ ├── ExceptionHandlingMiddleware.cs ← Zentrale Exception Handling ⭐ │ ├── TenantResolutionMiddleware.cs ← API-Key → Tenant │ └── RequestLoggingMiddleware.cs ← Request/Response Logging ├── Configuration/ │ ├── SwaggerConfiguration.cs ← Swagger Setup (API-Key Support) │ └── SerilogConfiguration.cs ← Serilog Helper ├── appsettings.json ← Base Configuration ├── appsettings.Development.json ← Dev Overrides └── Program.cs ← Application Entry Point ``` **Was gehört hierher?** - ✅ HTTP Routing (Minimal APIs) - ✅ Middleware (Exception, Auth, Logging) - ✅ Swagger Configuration - ✅ Dependency Injection Setup - ✅ appsettings.json **Was NICHT hierher gehört?** - ❌ Business Logic (→ Application/Domain) - ❌ PDF-Verarbeitung (→ Infrastructure) - ❌ Validierung (→ Application: FluentValidation) --- ### 💼 Application Layer (DocumentOperator.Application) **Purpose:** Use Cases, Business Logic Orchestration **References:** - → Domain (ONLY!) **NuGet Packages:** - MediatR - FluentValidation + DI Extensions **Folder Structure:** ``` DocumentOperator.Application/ ├── Features/ ← Vertical Slices │ └── Documents/ │ ├── ProcessDocument/ │ │ ├── ProcessDocumentCommand.cs │ │ ├── ProcessDocumentHandler.cs │ │ └── ProcessDocumentValidator.cs │ ├── ValidatePdf/ │ │ ├── ValidatePdfQuery.cs │ │ ├── ValidatePdfHandler.cs │ │ └── ValidatePdfValidator.cs │ ├── ExtractAttachments/ │ │ ├── ExtractAttachmentsCommand.cs │ │ ├── ExtractAttachmentsHandler.cs │ │ └── ExtractAttachmentsValidator.cs │ ├── ConcatenatePdfs/ │ │ ├── ConcatenatePdfsCommand.cs │ │ ├── ConcatenatePdfsHandler.cs │ │ └── ConcatenatePdfsValidator.cs │ ├── ApplyStamp/ │ │ ├── ApplyStampCommand.cs │ │ ├── ApplyStampHandler.cs │ │ └── ApplyStampValidator.cs │ └── EmbedCertificate/ │ ├── EmbedCertificateCommand.cs │ ├── EmbedCertificateHandler.cs │ └── EmbedCertificateValidator.cs ├── Common/ │ ├── Interfaces/ ← Abstractions für Infrastructure │ │ ├── IPdfProcessor.cs │ │ ├── IFileStorageService.cs │ │ ├── IDocumentValidator.cs │ │ └── ICertificateService.cs │ ├── Behaviors/ ← MediatR Pipeline Behaviors │ │ ├── ValidationBehavior.cs ← FluentValidation Integration ⭐ │ │ ├── LoggingBehavior.cs │ │ └── ExceptionLoggingBehavior.cs │ ├── DTOs/ ← Data Transfer Objects │ │ ├── ProcessDocumentRequest.cs │ │ ├── ProcessDocumentResponse.cs │ │ ├── DocumentOperationDto.cs │ │ ├── AttachmentDto.cs │ │ └── ErrorResponse.cs ← API Error Format │ └── Mappings/ ← Domain ↔ DTO │ └── MappingExtensions.cs └── DependencyInjection.cs ← Service Registration ``` **Was gehört hierher?** - ✅ MediatR Commands & Queries - ✅ Handlers (orchestrieren Domain + Infrastructure) - ✅ FluentValidation Validators - ✅ DTOs (API Contracts) - ✅ Interfaces für Infrastructure (Dependency Inversion!) - ✅ Pipeline Behaviors **Was NICHT hierher gehört?** - ❌ DevExpress-spezifischer Code (→ Infrastructure) - ❌ File I/O (→ Infrastructure) - ❌ HTTP-spezifisches (→ API) - ❌ EF Core / Database (haben wir nicht) **Warum keine Infrastructure-Referenz?** - Clean Architecture: Application kennt nur **Interfaces** (`IPdfProcessor`) - Infrastructure **implementiert** die Interfaces (`DevExpressPdfProcessor`) - API injected die Implementierung via DI - → Application bleibt technologie-unabhängig! --- ### 🔧 Infrastructure Layer (DocumentOperator.Infrastructure) **Purpose:** Technische Implementierungen, externe Abhängigkeiten **References:** - → Application (für Interfaces) - → Domain **NuGet Packages:** - DevExpress.Pdf.Core - Microsoft.Extensions.Options.ConfigurationExtensions **Folder Structure:** ``` DocumentOperator.Infrastructure/ ├── Services/ │ ├── PdfProcessing/ │ │ └── DevExpressPdfProcessor.cs ← IPdfProcessor Implementation │ ├── FileStorage/ │ │ └── LocalFileStorageService.cs ← IFileStorageService Implementation │ └── DocumentValidation/ │ └── PdfDocumentValidator.cs ← IDocumentValidator Implementation ├── Configuration/ │ ├── DocumentOperatorSettings.cs ← Options Pattern Class │ ├── RedisSettings.cs │ ├── ApiKeySettings.cs │ └── TenantInfo.cs └── DependencyInjection.cs ← Service Registration ``` **Was gehört hierher?** - ✅ DevExpress Integration - ✅ File System Zugriffe (Temp-Files) - ✅ Redis Client (später) - ✅ Externe API Calls (falls benötigt) - ✅ Options Pattern Classes **Was NICHT hierher gehört?** - ❌ Business Logic (→ Application/Domain) - ❌ HTTP Handling (→ API) - ❌ Validierung von Inputs (→ Application) **Beispiel - DevExpressPdfProcessor:** ```csharp public class DevExpressPdfProcessor : IPdfProcessor { public async Task MergePdfsAsync(List pdfs) { using var processor = new PdfDocumentProcessor(); // DevExpress! // ... DevExpress-spezifischer Code if (error) throw new PdfProcessingException("Merge failed"); // Exception! return result; } } ``` --- ### 🏛️ Domain Layer (DocumentOperator.Domain) **Purpose:** Kern-Geschäftslogik, Business Rules **References:** - → **KEINE!** (wichtigste Clean Architecture Regel) **NuGet Packages:** - **KEINE!** (reine C# Klassen) **Folder Structure:** ``` DocumentOperator.Domain/ ├── Models/ │ ├── PdfDocument.cs ← Core Business Model │ ├── DocumentAttachment.cs │ ├── DocumentStamp.cs │ └── DocumentCertificate.cs ├── Models/ValueObjects/ ← Immutable, selbst-validierend │ ├── Base64String.cs ← Wirft DomainValidationException │ ├── TenantId.cs │ └── PdfMetadata.cs ├── Models/Enums/ │ ├── DocumentOperationType.cs ← Extract, Concatenate, Stamp, Sign │ ├── ProcessingStatus.cs ← Pending, Processing, Success, Failed │ └── PdfValidationError.cs ← InvalidFormat, TooLarge, Corrupted ├── Common/ │ └── Exceptions/ ← Domain-spezifische Exceptions │ ├── DomainException.cs ← Basis-Exception │ ├── DomainValidationException.cs← Value Object Validierung │ ├── NotFoundException.cs ← Resource nicht gefunden │ └── PdfProcessingException.cs ← PDF-spezifische Fehler └── Constants/ ├── ErrorCodes.cs ← Konstanten für Error Messages └── ValidationMessages.cs ``` **Was gehört hierher?** - ✅ Business Models (PdfDocument, etc.) - ✅ Value Objects (Base64String, TenantId) - ✅ Enums (DocumentOperationType) - ✅ Business Rules (z.B. "Max 100 Seiten") - ✅ Domain Exceptions - ✅ Constants **Was NICHT hierher gehört?** - ❌ DevExpress (→ Infrastructure) - ❌ MediatR (→ Application) - ❌ DTOs (→ Application) - ❌ Validation Logic (→ Application: FluentValidation) - ❌ JEGLICHE externe Library! **Warum keine Dependencies?** - Domain = Herz der Anwendung - Sollte **ewig** leben (auch wenn Tech-Stack wechselt) - Keine Abhängigkeit von Frameworks = langlebig - Pure C# Business Logic --- ## 🗺️ DEVELOPMENT ROADMAP --- ### ✅ **PHASE 1: Foundation & Clean Architecture Setup** - **COMPLETED** **Ziel:** Saubere Architektur-Basis ohne Funktionalität #### ✅ Step 1.1: Projekt-Dependencies korrigieren - **DONE** - [x] Application: Infrastructure-Referenz entfernt - [x] Infrastructure: Application-Referenz hinzugefügt - [x] Verify: Domain hat keine Dependencies - [x] Build: Erfolgreich **Ergebnis:** Clean Architecture Dependency Rules eingehalten --- #### ✅ Step 1.2: NuGet Packages installieren - **DONE** **Application:** - [x] MediatR (14.1.0) - [x] FluentValidation (12.1.1) - [x] FluentValidation.DependencyInjectionExtensions (12.1.1) - [x] ~~Ardalis.Result (10.1.0)~~ → **ENTFERNT** (Exception-basiert) **Infrastructure:** - [x] DevExpress.Pdf.Core (25.2.8) - [x] Microsoft.Extensions.Options.ConfigurationExtensions (8.0.0) **API:** - [x] Serilog.AspNetCore (10.0.0) - [x] Serilog.Enrichers.Environment (3.0.1) - [x] Serilog.Sinks.File (7.0.0) - [x] Asp.Versioning.Http (8.1.1) - [x] Microsoft.Extensions.Caching.StackExchangeRedis (8.0.28) - [x] Swashbuckle.AspNetCore (6.6.2) **Ergebnis:** Alle Packages installiert, neueste stable Versionen --- #### ✅ Step 1.3: Folder-Struktur erstellen - **DONE** **Domain:** - [x] Models/ - [x] Models/ValueObjects/ - [x] Models/Enums/ - [x] Common/ - [x] Common/Exceptions/ - [x] Constants/ **Application:** - [x] Features/ - [x] Features/Documents/ - [x] Features/Documents/ProcessDocument/ - [x] Features/Documents/ValidatePdf/ - [x] Features/Documents/ExtractAttachments/ - [x] Features/Documents/ConcatenatePdfs/ - [x] Features/Documents/ApplyStamp/ - [x] Features/Documents/EmbedCertificate/ - [x] Common/ - [x] Common/Interfaces/ - [x] Common/Behaviors/ - [x] Common/DTOs/ - [x] Common/Mappings/ **Infrastructure:** - [x] Services/ - [x] Services/PdfProcessing/ - [x] Services/FileStorage/ - [x] Services/DocumentValidation/ - [x] Configuration/ **API:** - [x] Endpoints/ - [x] Endpoints/v1/ - [x] Middleware/ - [x] Configuration/ - [x] Controllers/ → **GELÖSCHT** (Minimal APIs!) **Ergebnis:** Komplette Ordnerstruktur nach Clean Architecture --- #### ✅ Step 1.4: Basis-Configuration - **DONE** **Part A: appsettings.json** - [x] appsettings.json mit allen Settings erstellt - Serilog Configuration - DocumentOperatorSettings - RedisSettings - ApiKeySettings (Demo-Keys) - [x] appsettings.Development.json für Dev-Overrides - [x] .gitignore vorhanden (bereits existiert) **Part B: Options Classes** - [x] DocumentOperatorSettings.cs - [x] RedisSettings.cs - [x] ApiKeySettings.cs - [x] TenantInfo.cs (als separate Klasse ✅ Best Practice!) **Part C: Serilog Setup** - [x] Program.cs erweitert mit Serilog - [x] Options Pattern registriert - [x] Try/Catch für Startup-Errors - [x] Serilog Request Logging aktiviert - [x] Serilog Enrichers installiert **Ergebnis:** Produktionsreife Konfiguration, Logging funktioniert --- ### 🔄 **PHASE 2: Domain Layer** - **IN PROGRESS** **Ziel:** Business Models ohne technische Dependencies erstellen --- #### ✅ Step 2.1: Domain Exceptions erstellen - **COMPLETED** **Aufgabe:** Custom Exception-Klassen für fachliche Fehler **Erstellt:** 1. [x] `DomainException.cs` (Basis-Exception) 2. [x] `DomainValidationException.cs` (Value Object Validierung) 3. [x] `NotFoundException.cs` (Resource nicht gefunden) 4. [x] `PdfProcessingException.cs` (PDF-spezifische Fehler) **Wo:** `Domain/Common/Exceptions/` **Warum Exceptions?** - Zentrale Exception Handling Middleware (API Layer) - Einfacher Code (keine Result Checks) - Standard .NET Exception-Flow - Wartbar an einer Stelle **Verwendung:** ```csharp // In Value Objects: if (invalid) throw new DomainValidationException("Base64 cannot be empty"); // In Handlers: if (notFound) throw new NotFoundException("Document", id); // Middleware fängt ab und mapped zu HTTP 400/404/500 ``` --- #### 🔄 Step 2.2: Value Objects erstellen - **NEXT** **Aufgabe:** Typsichere, selbst-validierende Wert-Objekte **Zu erstellen:** 1. [ ] `Base64String.cs` - Factory Method: `Create(string value)` - Validierung: Gültiges Base64-Format - Konvertierung: `ToByteArray()`, `FromByteArray()` - Wirft `DomainValidationException` bei Fehler 2. [ ] `TenantId.cs` - Factory Method: `Create(string value)` - Validierung: Nicht leer, Max 100 Zeichen - Normalisierung: `.ToLowerInvariant()` - Wirft `DomainValidationException` bei Fehler 3. [ ] `PdfMetadata.cs` - Properties: PageCount, FileSizeBytes, PdfVersion, HasAttachments - Computed Property: `FileSizeMB` - Keine Validierung (nur Daten-Container) **Wo:** `Domain/Models/ValueObjects/` **Warum Value Objects?** - ✅ Typsicherheit: `Base64String` statt `string` - ✅ Validierung an **einer** Stelle (Constructor) - ✅ Immutable (keine Änderungen nach Erstellung) - ✅ Domain-Driven Design Best Practice --- #### ⏳ Step 2.3: Enums erstellen **Aufgabe:** Aufzählungen für Business-Konzepte **Zu erstellen:** 1. [ ] `DocumentOperationType.cs` ```csharp public enum DocumentOperationType { Validate, ExtractAttachments, Concatenate, ApplyStamp, EmbedCertificate } ``` 2. [ ] `ProcessingStatus.cs` ```csharp public enum ProcessingStatus { Pending, Processing, Success, Failed } ``` 3. [ ] `PdfValidationError.cs` ```csharp public enum PdfValidationError { InvalidFormat, FileTooLarge, Corrupted, UnsupportedVersion, NoPages } ``` **Wo:** `Domain/Models/Enums/` --- #### ⏳ Step 2.4: Domain Models erstellen **Aufgabe:** Kern-Business-Objekte **Zu erstellen:** 1. [ ] `PdfDocument.cs` (Core Model) - Properties: Id, Base64Content, Metadata, Attachments, Status - Methods: AddAttachment(), ApplyStamp(), etc. 2. [ ] `DocumentAttachment.cs` - Properties: FileName, Content (Base64), FileSize, MimeType 3. [ ] `DocumentStamp.cs` - Properties: Text, Position, Logo (Base64), TenantId 4. [ ] `DocumentCertificate.cs` - Properties: PfxContent (Base64), Password, Issuer **Wo:** `Domain/Models/` **Warum Domain Models?** - Repräsentieren Business-Konzepte - Enthalten Business Rules - Keine Datenbank-Annotations (wir haben kein EF Core!) - Pure C# Klassen --- #### ⏳ Step 2.5: Constants erstellen **Aufgabe:** Konstanten für Error Messages, Limits, etc. **Zu erstellen:** 1. [ ] `ErrorCodes.cs` ```csharp public static class ErrorCodes { public const string InvalidBase64 = "ERR_INVALID_BASE64"; public const string PdfTooLarge = "ERR_PDF_TOO_LARGE"; public const string InvalidTenant = "ERR_INVALID_TENANT"; } ``` 2. [ ] `ValidationMessages.cs` ```csharp public static class ValidationMessages { public const string Base64Empty = "Base64 string cannot be empty"; public const string TenantIdEmpty = "TenantId is required"; } ``` **Wo:** `Domain/Constants/` **Warum Constants?** - Keine Magic Strings im Code - Wiederverwendbar - Leicht änderbar (an einer Stelle) --- ### ⏳ **PHASE 3: Application Layer** **Ziel:** Use Cases mit MediatR implementieren --- #### ⏳ Step 3.1: MediatR Setup & Behaviors **Aufgabe:** MediatR konfigurieren + Pipeline Behaviors **Zu erstellen:** 1. [ ] `DependencyInjection.cs` (Application Layer) - MediatR registrieren - FluentValidation registrieren - Behaviors registrieren 2. [ ] `ValidationBehavior.cs` ⭐ - Vor jedem Handler: FluentValidation ausführen - Bei Fehler: `ValidationException` werfen - Middleware fängt ab → HTTP 400 3. [ ] `LoggingBehavior.cs` - Request/Response loggen - Execution Time messen 4. [ ] `ExceptionLoggingBehavior.cs` - Exceptions loggen bevor sie propagieren **Wo:** `Application/Common/Behaviors/` **Warum Behaviors?** - Cross-Cutting Concerns (Validation, Logging) - DRY: Nicht in jedem Handler wiederholen - Pipeline Pattern --- #### ⏳ Step 3.2: Interfaces für Infrastructure **Aufgabe:** Abstractions definieren (Dependency Inversion!) **Zu erstellen:** 1. [ ] `IPdfProcessor.cs` ```csharp public interface IPdfProcessor { Task MergePdfsAsync(List pdfs); Task> ExtractAttachmentsAsync(PdfDocument pdf); Task ApplyStampAsync(PdfDocument pdf, DocumentStamp stamp); Task EmbedCertificateAsync(PdfDocument pdf, DocumentCertificate cert); } ``` 2. [ ] `IFileStorageService.cs` ```csharp public interface IFileStorageService { Task SaveTempFileAsync(byte[] content, string extension); Task LoadTempFileAsync(string path); Task DeleteTempFileAsync(string path); Task CleanupOldFilesAsync(TimeSpan maxAge); } ``` 3. [ ] `IDocumentValidator.cs` ```csharp public interface IDocumentValidator { Task ValidateAsync(PdfDocument pdf); bool IsValidFormat(byte[] content); } ``` 4. [ ] `ICertificateService.cs` ```csharp public interface ICertificateService { Task ValidateCertificateAsync(DocumentCertificate cert); } ``` **Wo:** `Application/Common/Interfaces/` **Warum Interfaces?** - Application kennt nur Verträge (nicht Implementierung) - Infrastructure implementiert - Testbar (Mocking) - Clean Architecture Dependency Rule --- #### ⏳ Step 3.3: DTOs erstellen **Aufgabe:** Data Transfer Objects für API **Zu erstellen:** 1. [ ] `ProcessDocumentRequest.cs` (Record Type) ```csharp public record ProcessDocumentRequest( string Base64Pdf, string TenantId, List Operations ); ``` 2. [ ] `ProcessDocumentResponse.cs` ```csharp public record ProcessDocumentResponse( string Base64Pdf, PdfMetadata Metadata, List PerformedOperations, bool Success ); ``` 3. [ ] `DocumentOperationDto.cs` ```csharp public record DocumentOperationDto( DocumentOperationType Type, Dictionary? Parameters ); ``` 4. [ ] `AttachmentDto.cs` 5. [ ] `ErrorResponse.cs` (für Exception Middleware) **Wo:** `Application/Common/DTOs/` **Warum DTOs?** - API Contracts (können sich ändern ohne Domain zu ändern) - Validation (FluentValidation) - Serialization-friendly --- #### ⏳ Step 3.4: Erste Feature - ValidatePdf **Aufgabe:** Erste komplette Feature-Implementierung **Zu erstellen:** 1. [ ] `ValidatePdfQuery.cs` ```csharp public record ValidatePdfQuery(string Base64Pdf, string TenantId) : IRequest; ``` 2. [ ] `ValidatePdfHandler.cs` ```csharp public class ValidatePdfHandler : IRequestHandler { public async Task Handle(...) { var base64 = Base64String.Create(query.Base64Pdf); // Wirft Exception var pdf = new PdfDocument(base64); var metadata = await _validator.ValidateAsync(pdf); return metadata; } } ``` 3. [ ] `ValidatePdfValidator.cs` (FluentValidation) ```csharp public class ValidatePdfValidator : AbstractValidator { public ValidatePdfValidator() { RuleFor(x => x.Base64Pdf).NotEmpty(); RuleFor(x => x.TenantId).NotEmpty(); } } ``` **Wo:** `Application/Features/Documents/ValidatePdf/` **Flow:** ``` API → ValidatePdfQuery → ValidationBehavior (FluentValidation) → ValidatePdfHandler → IDocumentValidator (Infrastructure) → PdfMetadata zurück ``` --- #### ⏳ Step 3.5: Weitere Features Nach ValidatePdf (als Beispiel): - [ ] ProcessDocument (orchestriert andere Commands) - [ ] ExtractAttachments - [ ] ConcatenatePdfs - [ ] ApplyStamp - [ ] EmbedCertificate Jeweils: Command/Query + Handler + Validator --- ### ⏳ **PHASE 4: Infrastructure Layer** **Ziel:** Interfaces implementieren mit echten Technologien --- #### ⏳ Step 4.1: DevExpress PDF Service **Aufgabe:** `IPdfProcessor` implementieren **Zu erstellen:** 1. [ ] `DevExpressPdfProcessor.cs : IPdfProcessor` ```csharp public class DevExpressPdfProcessor : IPdfProcessor { public async Task MergePdfsAsync(List pdfs) { try { using var processor = new PdfDocumentProcessor(); // DevExpress! foreach (var pdf in pdfs) { processor.AppendDocument(pdf.ToStream()); } var result = processor.SaveDocument(); return new PdfDocument(result); } catch (Exception ex) { throw new PdfProcessingException("PDF merge failed", ex); } } // ExtractAttachments(), ApplyStamp(), EmbedCertificate() ... } ``` **Wo:** `Infrastructure/Services/PdfProcessing/` **Wichtig:** - Wirft `PdfProcessingException` bei Fehlern - Nutzt DevExpress API - Async wo möglich --- #### ⏳ Step 4.2: File Storage Service **Aufgabe:** `IFileStorageService` implementieren **Zu erstellen:** 1. [ ] `LocalFileStorageService.cs : IFileStorageService` ```csharp public class LocalFileStorageService : IFileStorageService { private readonly DocumentOperatorSettings _settings; public async Task SaveTempFileAsync(byte[] content, string extension) { var fileName = $"{Guid.NewGuid()}{extension}"; var path = Path.Combine(_settings.TempFolderPath, fileName); Directory.CreateDirectory(_settings.TempFolderPath); await File.WriteAllBytesAsync(path, content); return path; } // LoadTempFileAsync(), DeleteTempFileAsync(), CleanupOldFilesAsync() } ``` **Wo:** `Infrastructure/Services/FileStorage/` **Features:** - Nutzt `DocumentOperatorSettings.TempFolderPath` - Cleanup für alte Dateien (Background Service später) - Exception Handling --- #### ⏳ Step 4.3: Document Validator **Aufgabe:** `IDocumentValidator` implementieren **Zu erstellen:** 1. [ ] `PdfDocumentValidator.cs : IDocumentValidator` ```csharp public class PdfDocumentValidator : IDocumentValidator { public async Task ValidateAsync(PdfDocument pdf) { using var processor = new PdfDocumentProcessor(); processor.LoadDocument(pdf.ToStream()); if (processor.Document.Pages.Count == 0) throw new PdfProcessingException("PDF has no pages"); return new PdfMetadata( pageCount: processor.Document.Pages.Count, fileSizeBytes: pdf.Content.Length, pdfVersion: processor.Document.Version.ToString(), hasAttachments: processor.Document.Attachments.Count > 0, attachmentCount: processor.Document.Attachments.Count ); } } ``` **Wo:** `Infrastructure/Services/DocumentValidation/` --- #### ⏳ Step 4.4: DI Registration **Aufgabe:** Services registrieren **Zu erstellen:** 1. [ ] `DependencyInjection.cs` (Infrastructure Layer) ```csharp public static class DependencyInjection { public static IServiceCollection AddInfrastructure( this IServiceCollection services) { services.AddScoped(); services.AddScoped(); services.AddScoped(); return services; } } ``` **In Program.cs aufrufen:** ```csharp builder.Services.AddInfrastructure(); ``` --- ### ⏳ **PHASE 5: API Layer - Minimal APIs & Middleware** **Ziel:** HTTP Endpoints + Exception Handling --- #### ⏳ Step 5.1: Exception Handling Middleware ⭐ **Aufgabe:** Zentrale Exception → HTTP Response Mapping **Zu erstellen:** 1. [ ] `ExceptionHandlingMiddleware.cs` ```csharp public class ExceptionHandlingMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; public async Task InvokeAsync(HttpContext context) { try { await _next(context); } catch (DomainValidationException ex) { _logger.LogWarning(ex, "Validation error"); await HandleValidationExceptionAsync(context, ex); } catch (NotFoundException ex) { _logger.LogWarning(ex, "Resource not found"); await HandleNotFoundExceptionAsync(context, ex); } catch (PdfProcessingException ex) { _logger.LogError(ex, "PDF processing failed"); await HandlePdfProcessingExceptionAsync(context, ex); } catch (ValidationException ex) // FluentValidation { _logger.LogWarning(ex, "Input validation failed"); await HandleFluentValidationExceptionAsync(context, ex); } catch (Exception ex) { _logger.LogCritical(ex, "Unhandled exception"); await HandleUnexpectedExceptionAsync(context, ex); } } private static Task HandleValidationExceptionAsync(HttpContext context, DomainValidationException ex) { context.Response.StatusCode = StatusCodes.Status400BadRequest; var problemDetails = new ProblemDetails { Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", Title = "Validation Error", Status = StatusCodes.Status400BadRequest, Detail = ex.Message, Instance = context.Request.Path }; return context.Response.WriteAsJsonAsync(problemDetails); } // HandleNotFoundException(), HandlePdfProcessingException(), etc. } ``` **Wo:** `API/Middleware/` **Registrieren in Program.cs:** ```csharp app.UseMiddleware(); ``` **Warum zentral?** - ✅ Alle Fehler an **einer** Stelle - ✅ Konsistente Error-Responses - ✅ Logging zentral - ✅ HTTP Status Code Mapping - ✅ Wartbar! --- #### ⏳ Step 5.2: Minimal API Endpoints **Aufgabe:** HTTP Endpoints definieren **Zu erstellen:** 1. [ ] `DocumentEndpoints.cs` ```csharp public static class DocumentEndpoints { public static IEndpointRouteBuilder MapDocumentEndpoints( this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/v1/documents") .WithTags("Documents") .WithOpenApi(); group.MapPost("/validate", ValidatePdf) .WithName("ValidatePdf") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest); group.MapPost("/process", ProcessDocument) .WithName("ProcessDocument"); group.MapPost("/extract-attachments", ExtractAttachments); group.MapPost("/concatenate", ConcatenatePdfs); group.MapPost("/stamp", ApplyStamp); group.MapPost("/sign", EmbedCertificate); return app; } private static async Task ValidatePdf( ValidatePdfRequest request, IMediator mediator, CancellationToken ct) { var query = new ValidatePdfQuery(request.Base64Pdf, request.TenantId); var result = await mediator.Send(query, ct); return Results.Ok(result); } // ProcessDocument(), ExtractAttachments(), etc. } ``` **Wo:** `API/Endpoints/v1/` **In Program.cs registrieren:** ```csharp app.MapDocumentEndpoints(); ``` **Features:** - Minimal APIs (keine Controller-Klassen!) - OpenAPI/Swagger Integration - MediatR aufrufen - Middleware fängt Exceptions ab --- #### ⏳ Step 5.3: Swagger Configuration **Aufgabe:** Swagger mit API-Key Support **Zu erstellen:** 1. [ ] `SwaggerConfiguration.cs` ```csharp public static class SwaggerConfiguration { public static IServiceCollection AddSwaggerConfiguration( this IServiceCollection services) { services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "DocumentOperator API", Version = "v1", Description = "Zentralisierter PDF-Operationen Service" }); // API-Key Support c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme { Description = "API Key (Header: X-API-Key)", Name = "X-API-Key", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, Scheme = "ApiKeyScheme" }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "ApiKey" } }, Array.Empty() } }); }); return services; } } ``` **Wo:** `API/Configuration/` **Ergebnis:** - Swagger UI zeigt "Authorize" Button - Dev-Leiter kann API-Key eingeben für Tests - Alle Requests enthalten X-API-Key Header --- #### ⏳ Step 5.4: Tenant Resolution Middleware **Aufgabe:** API-Key → Tenant Context **Zu erstellen:** 1. [ ] `TenantResolutionMiddleware.cs` ```csharp public class TenantResolutionMiddleware { public async Task InvokeAsync(HttpContext context) { var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault(); if (string.IsNullOrEmpty(apiKey)) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; await context.Response.WriteAsJsonAsync(new ProblemDetails { Title = "API Key Missing", Status = 401 }); return; } // API-Key → Tenant auflösen (aus ApiKeySettings) var tenantInfo = _apiKeySettings.Keys.GetValueOrDefault(apiKey); if (tenantInfo == null || !tenantInfo.IsActive) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; return; } // Tenant-Context in DI (Scoped) var tenantContext = context.RequestServices.GetRequiredService(); tenantContext.SetTenant(tenantInfo.TenantId, tenantInfo.TenantName); await _next(context); } } ``` **Wo:** `API/Middleware/` **Flow:** ``` Request mit X-API-Key → Middleware → API-Key validieren → Tenant auflösen → TenantContext setzen → Handler nutzt TenantContext ``` --- ### ⏳ **PHASE 6: Multi-Tenancy & Security** **Ziel:** Mandantenfähigkeit implementieren #### ⏳ Steps: - [ ] `ITenantContext` Interface (Application) - [ ] `TenantContext` Implementation (Infrastructure) - [ ] Rate Limiting pro Tenant - [ ] Tenant-spezifische Settings (Logo, Zertifikat) --- ### ⏳ **PHASE 7: Distributed Cache (Redis)** **Ziel:** Performance-Optimierung #### ⏳ Steps: - [ ] Redis Connection Setup - [ ] Cache für API-Key Validation - [ ] Cache für Tenant-Settings - [ ] Cache Invalidation Strategy --- ### ⏳ **PHASE 8: Testing** **Ziel:** Qualitätssicherung #### ⏳ Steps: - [ ] Unit Tests (Application Handlers) - [ ] Integration Tests (API Endpoints) - [ ] Test-PDFs erstellen - [ ] Coverage >80% --- ### ⏳ **PHASE 9: Deployment (IIS)** **Ziel:** Production-Ready #### ⏳ Steps: - [ ] appsettings.Production.json - [ ] IIS Application Pool (.NET 8) - [ ] HTTPS Binding - [ ] Environment Variables - [ ] Health Checks --- ## 📊 CURRENT STATUS ### ✅ Completed - **Phase 1:** Foundation & Clean Architecture Setup - Dependencies ✅ - Packages ✅ - Folder Structure ✅ - Configuration ✅ - Serilog ✅ ### 🔄 In Progress - **Phase 2:** Domain Layer - ✅ Step 2.1 - Domain Exceptions - **NEXT:** Step 2.2 - Value Objects ### ⏳ Pending - Phase 3-9 --- ## 🎓 LEARNING NOTES ### Clean Architecture Principles Learned 1. **Dependency Rule:** Immer nach innen (Domain kennt nichts, Application nur Domain, etc.) 2. **Separation of Concerns:** Jede Schicht hat klare Verantwortung 3. **Value Objects:** Typsicherheit + Validierung in einem 4. **CQRS:** Klare Trennung Commands/Queries 5. **Vertical Slices:** Feature-basiert statt Layer-basiert ### Exception-based Error Handling **Vorteile erkannt:** - Einfacherer Code (kein Result Boilerplate) - Zentrales Handling (Middleware) - Wartbarer (Fehler-Mapping an einer Stelle) - Standard .NET Flow **Wichtig:** - FluentValidation für Input (erste Verteidigung) - Domain Exceptions für Business-Fehler - Middleware mapped zu HTTP Status Codes - Serilog loggt alles --- ## 📚 REFERENCES & RESOURCES ### Documentation - [Clean Architecture (Uncle Bob)](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - [MediatR Documentation](https://github.com/jbogard/MediatR) - [FluentValidation Docs](https://docs.fluentvalidation.net/) - [DevExpress PDF API](https://docs.devexpress.com/OfficeFileAPI/114877/pdf-document-api) - [ASP.NET Core Minimal APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis) - [RFC 7807 Problem Details](https://datatracker.ietf.org/doc/html/rfc7807) ### Best Practices Applied - ✅ Vertical Slice Architecture - ✅ Options Pattern für Configuration - ✅ Dependency Injection - ✅ Async/Await überall - ✅ Nullable Reference Types - ✅ Record Types für DTOs (C# 12) - ✅ Primary Constructors (.NET 8) - ✅ Structured Logging (Serilog) --- ## 🔄 UPDATE LOG | Date | Phase | Changes | |------|-------|---------| | 2024-XX-XX | Phase 1 | Project setup, dependencies, folder structure | | 2024-XX-XX | Phase 1 | Configuration, Serilog, Options Pattern | | 2024-XX-XX | Phase 1 | **Decision:** Exception-based statt Ardalis.Result | | 2024-XX-XX | Phase 1 | ✅ Phase 1 completed | | 2024-XX-XX | Phase 2 | ✅ Step 2.1 completed - Domain Exceptions created | | 2024-XX-XX | Phase 2 | 🔄 Starting Step 2.2 - Value Objects | --- **END OF ROADMAP** *This document is a living document and will be updated as development progresses.*