- Added "Last Updated" timestamp, current status, and phase. - Introduced a "TABLE OF CONTENTS" for easier navigation. - Expanded "PROJECT OVERVIEW" with vision, purpose, and workflow. - Detailed "ARCHITECTURE & DESIGN DECISIONS" with key patterns. - Listed frameworks, libraries, and components in "TECH STACK." - Provided a breakdown of the solution's folder structure. - Outlined development phases in "DEVELOPMENT ROADMAP." - Documented progress in "CURRENT STATUS" and added "UPDATE LOG." - Included "LEARNING NOTES" and references to best practices. - Improved formatting for clarity and readability.
44 KiB
📘 DocumentOperator - Project Roadmap
Last Updated: 16.06.2026 | Status: In Development | Phase: 2 (Domain Layer)
📋 TABLE OF CONTENTS
- Project Overview
- Architecture & Design Decisions
- Technology Stack
- Project Structure
- Development Roadmap
- 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:
- Client sendet PDF als Base64 in JSON
- API validiert Input (FluentValidation)
- PDF wird in Byte-Array konvertiert
- Operationen werden durchgeführt (DevExpress)
- Temporäre Dateien werden erstellt/bereinigt
- 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:
public class DocumentService {
public void Process() { }
public void Validate() { }
public void Extract() { }
// ... 20 Methoden
}
Nutzen wir:
// Feature: ProcessDocument
public class ProcessDocumentCommand : IRequest<PdfDocument> { }
public class ProcessDocumentHandler : IRequestHandler<ProcessDocumentCommand, PdfDocument> { }
public class ProcessDocumentValidator : AbstractValidator<ProcessDocumentCommand> { }
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:
- Domain Exceptions für fachliche Fehler
- FluentValidation für Input-Validierung
- 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:
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 |
| ❌ 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:
public class DevExpressPdfProcessor : IPdfProcessor
{
public async Task<PdfDocument> MergePdfsAsync(List<PdfDocument> 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
- Application: Infrastructure-Referenz entfernt
- Infrastructure: Application-Referenz hinzugefügt
- Verify: Domain hat keine Dependencies
- Build: Erfolgreich
Ergebnis: Clean Architecture Dependency Rules eingehalten
✅ Step 1.2: NuGet Packages installieren - DONE
Application:
- MediatR (14.1.0)
- FluentValidation (12.1.1)
- FluentValidation.DependencyInjectionExtensions (12.1.1)
Ardalis.Result (10.1.0)→ ENTFERNT (Exception-basiert)
Infrastructure:
- DevExpress.Pdf.Core (25.2.8)
- Microsoft.Extensions.Options.ConfigurationExtensions (8.0.0)
API:
- Serilog.AspNetCore (10.0.0)
- Serilog.Enrichers.Environment (3.0.1)
- Serilog.Sinks.File (7.0.0)
- Asp.Versioning.Http (8.1.1)
- Microsoft.Extensions.Caching.StackExchangeRedis (8.0.28)
- Swashbuckle.AspNetCore (6.6.2)
Ergebnis: Alle Packages installiert, neueste stable Versionen
✅ Step 1.3: Folder-Struktur erstellen - DONE
Domain:
- Models/
- Models/ValueObjects/
- Models/Enums/
- Common/
- Common/Exceptions/
- Constants/
Application:
- Features/
- Features/Documents/
- Features/Documents/ProcessDocument/
- Features/Documents/ValidatePdf/
- Features/Documents/ExtractAttachments/
- Features/Documents/ConcatenatePdfs/
- Features/Documents/ApplyStamp/
- Features/Documents/EmbedCertificate/
- Common/
- Common/Interfaces/
- Common/Behaviors/
- Common/DTOs/
- Common/Mappings/
Infrastructure:
- Services/
- Services/PdfProcessing/
- Services/FileStorage/
- Services/DocumentValidation/
- Configuration/
API:
- Endpoints/
- Endpoints/v1/
- Middleware/
- Configuration/
- Controllers/ → GELÖSCHT (Minimal APIs!)
Ergebnis: Komplette Ordnerstruktur nach Clean Architecture
✅ Step 1.4: Basis-Configuration - DONE
Part A: appsettings.json
- appsettings.json mit allen Settings erstellt
- Serilog Configuration
- DocumentOperatorSettings
- RedisSettings
- ApiKeySettings (Demo-Keys)
- appsettings.Development.json für Dev-Overrides
- .gitignore vorhanden (bereits existiert)
Part B: Options Classes
- DocumentOperatorSettings.cs
- RedisSettings.cs
- ApiKeySettings.cs
- TenantInfo.cs (als separate Klasse ✅ Best Practice!)
Part C: Serilog Setup
- Program.cs erweitert mit Serilog
- Options Pattern registriert
- Try/Catch für Startup-Errors
- Serilog Request Logging aktiviert
- 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:
DomainException.cs(Basis-Exception)DomainValidationException.cs(Value Object Validierung)NotFoundException.cs(Resource nicht gefunden)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:
// 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:
-
Base64String.cs- Factory Method:
Create(string value) - Validierung: Gültiges Base64-Format
- Konvertierung:
ToByteArray(),FromByteArray() - Wirft
DomainValidationExceptionbei Fehler
- Factory Method:
-
TenantId.cs- Factory Method:
Create(string value) - Validierung: Nicht leer, Max 100 Zeichen
- Normalisierung:
.ToLowerInvariant() - Wirft
DomainValidationExceptionbei Fehler
- Factory Method:
-
PdfMetadata.cs- Properties: PageCount, FileSizeBytes, PdfVersion, HasAttachments
- Computed Property:
FileSizeMB - Keine Validierung (nur Daten-Container)
Wo: Domain/Models/ValueObjects/
Warum Value Objects?
- ✅ Typsicherheit:
Base64Stringstattstring - ✅ 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:
-
DocumentOperationType.cspublic enum DocumentOperationType { Validate, ExtractAttachments, Concatenate, ApplyStamp, EmbedCertificate } -
ProcessingStatus.cspublic enum ProcessingStatus { Pending, Processing, Success, Failed } -
PdfValidationError.cspublic enum PdfValidationError { InvalidFormat, FileTooLarge, Corrupted, UnsupportedVersion, NoPages }
Wo: Domain/Models/Enums/
⏳ Step 2.4: Domain Models erstellen
Aufgabe: Kern-Business-Objekte
Zu erstellen:
-
PdfDocument.cs(Core Model)- Properties: Id, Base64Content, Metadata, Attachments, Status
- Methods: AddAttachment(), ApplyStamp(), etc.
-
DocumentAttachment.cs- Properties: FileName, Content (Base64), FileSize, MimeType
-
DocumentStamp.cs- Properties: Text, Position, Logo (Base64), TenantId
-
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:
-
ErrorCodes.cspublic 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"; } -
ValidationMessages.cspublic 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:
-
DependencyInjection.cs(Application Layer)- MediatR registrieren
- FluentValidation registrieren
- Behaviors registrieren
-
ValidationBehavior.cs⭐- Vor jedem Handler: FluentValidation ausführen
- Bei Fehler:
ValidationExceptionwerfen - Middleware fängt ab → HTTP 400
-
LoggingBehavior.cs- Request/Response loggen
- Execution Time messen
-
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:
-
IPdfProcessor.cspublic interface IPdfProcessor { Task<PdfDocument> MergePdfsAsync(List<PdfDocument> pdfs); Task<List<DocumentAttachment>> ExtractAttachmentsAsync(PdfDocument pdf); Task<PdfDocument> ApplyStampAsync(PdfDocument pdf, DocumentStamp stamp); Task<PdfDocument> EmbedCertificateAsync(PdfDocument pdf, DocumentCertificate cert); } -
IFileStorageService.cspublic interface IFileStorageService { Task<string> SaveTempFileAsync(byte[] content, string extension); Task<byte[]> LoadTempFileAsync(string path); Task DeleteTempFileAsync(string path); Task CleanupOldFilesAsync(TimeSpan maxAge); } -
IDocumentValidator.cspublic interface IDocumentValidator { Task<PdfMetadata> ValidateAsync(PdfDocument pdf); bool IsValidFormat(byte[] content); } -
ICertificateService.cspublic interface ICertificateService { Task<bool> 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:
-
ProcessDocumentRequest.cs(Record Type)public record ProcessDocumentRequest( string Base64Pdf, string TenantId, List<DocumentOperationDto> Operations ); -
ProcessDocumentResponse.cspublic record ProcessDocumentResponse( string Base64Pdf, PdfMetadata Metadata, List<string> PerformedOperations, bool Success ); -
DocumentOperationDto.cspublic record DocumentOperationDto( DocumentOperationType Type, Dictionary<string, object>? Parameters ); -
AttachmentDto.cs -
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:
-
ValidatePdfQuery.cspublic record ValidatePdfQuery(string Base64Pdf, string TenantId) : IRequest<PdfMetadata>; -
ValidatePdfHandler.cspublic class ValidatePdfHandler : IRequestHandler<ValidatePdfQuery, PdfMetadata> { public async Task<PdfMetadata> Handle(...) { var base64 = Base64String.Create(query.Base64Pdf); // Wirft Exception var pdf = new PdfDocument(base64); var metadata = await _validator.ValidateAsync(pdf); return metadata; } } -
ValidatePdfValidator.cs(FluentValidation)public class ValidatePdfValidator : AbstractValidator<ValidatePdfQuery> { 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:
DevExpressPdfProcessor.cs : IPdfProcessor
public class DevExpressPdfProcessor : IPdfProcessor
{
public async Task<PdfDocument> MergePdfsAsync(List<PdfDocument> 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
PdfProcessingExceptionbei Fehlern - Nutzt DevExpress API
- Async wo möglich
⏳ Step 4.2: File Storage Service
Aufgabe: IFileStorageService implementieren
Zu erstellen:
LocalFileStorageService.cs : IFileStorageService
public class LocalFileStorageService : IFileStorageService
{
private readonly DocumentOperatorSettings _settings;
public async Task<string> 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:
PdfDocumentValidator.cs : IDocumentValidator
public class PdfDocumentValidator : IDocumentValidator
{
public async Task<PdfMetadata> 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:
DependencyInjection.cs(Infrastructure Layer)
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services)
{
services.AddScoped<IPdfProcessor, DevExpressPdfProcessor>();
services.AddScoped<IFileStorageService, LocalFileStorageService>();
services.AddScoped<IDocumentValidator, PdfDocumentValidator>();
return services;
}
}
In Program.cs aufrufen:
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:
ExceptionHandlingMiddleware.cs
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _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:
app.UseMiddleware<ExceptionHandlingMiddleware>();
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:
DocumentEndpoints.cs
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<PdfMetadata>(StatusCodes.Status200OK)
.Produces<ProblemDetails>(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<IResult> 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:
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:
SwaggerConfiguration.cs
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<string>()
}
});
});
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:
TenantResolutionMiddleware.cs
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<ITenantContext>();
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:
ITenantContextInterface (Application)TenantContextImplementation (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
- Dependency Rule: Immer nach innen (Domain kennt nichts, Application nur Domain, etc.)
- Separation of Concerns: Jede Schicht hat klare Verantwortung
- Value Objects: Typsicherheit + Validierung in einem
- CQRS: Klare Trennung Commands/Queries
- 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)
- MediatR Documentation
- FluentValidation Docs
- DevExpress PDF API
- ASP.NET Core Minimal APIs
- RFC 7807 Problem Details
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.