Files
DocumentService/DocumentOperator.API/ROADMAP.md
OlgunR 10cfb0c838 Refactor: Remove Azure dependencies, use local storage
Replaced Azure Blob Storage and Storage Queue with local
temp folders and an in-memory queue for file storage and
async processing. Updated `IFileStorage` and `IJobQueue`
interfaces to support the new architecture.

Modified `TenantSettings` and `ApplyStampHandler` to use
local file paths. Updated `JobProcessorService` to handle
in-memory queue jobs. Added file cleanup policies to
`LocalFileStorage`.

Revised roadmap and documentation to reflect the shift
to local-first architecture, emphasizing simplicity,
reduced cloud dependencies, and single-server readiness.
Logging now uses file-based storage instead of Application
Insights. Adjusted production deployment and health check
phases to align with the new approach.
2026-06-22 14:14:57 +02:00

2612 lines
80 KiB
Markdown

# ?? DocumentOperator - Project Roadmap (Pragmatic Edition)
> **Last Updated:** 22.06.2026 (Azure-Referenzen vollständig entfernt) | **Status:** In Development | **Phase:** 3 (Infrastructure Layer)
---
## ?? MAJOR UPDATE - Production-Ready Features Added!
**Was ist neu in diesem Update?**
1. **? Multi-Tenancy:** Database-based (EF Core + SQLite + Redis Cache) statt appsettings.json
2. **? Async Processing:** In-Memory Queue-based + Background Worker für große Operationen
3. **? File Storage:** Lokale Temp-Ordner mit IFileStorage Abstraction
4. **? Resilience:** Polly (Retry, Circuit Breaker, Timeout) für DevExpress Calls
5. **?? Health Checks:** FRÜH implementieren (Phase 5.5 statt Phase 9)
6. **? Logging:** Correlation IDs + Seq + File Logging
7. **? 9 neue NuGet Packages:** EF Core, Polly, BCrypt, Seq, Correlation IDs
8. **? 6 neue Phasen:** 5.5, 6.5, 8, 9, 10, 11 (insgesamt 11 Phasen statt 9)
9. **? 11 Key Learnings:** Dokumentiert (statt 5)
10. **? Technology Stack:** Komplett aktualisiert mit allen neuen Dependencies
**Warum diese Änderungen?**
- **Einfachheit:** Lokale Temp-Ordner (keine Cloud-Abhängigkeiten)
- **Security:** API-Key Hashing (BCrypt), Rotation möglich
- **Performance:** Async Processing für große Operationen (keine HTTP Timeouts)
- **Resilience:** Production-ready (Polly Retry/Circuit Breaker)
- **Monitoring:** Correlation IDs, Seq, File Logging
- **Wartbarkeit:** Clean Architecture bleibt pragmatisch, aber production-ready!
---
## ?? TABLE OF CONTENTS
1. [Project Overview](#project-overview)
2. [Architecture & Design Decisions](#architecture--design-decisions)
3. [Development Philosophy](#development-philosophy)
4. [Technology Stack](#technology-stack)
5. [Project Structure](#project-structure)
6. [Development Roadmap](#development-roadmap)
7. [Testing Strategy](#testing-strategy)
8. [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, Anhänge)
#### 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
**Synchroner Flow (kleine Operationen < 5 Sekunden):**
```
Client Application
?
[HTTP Request] - JSON mit Base64-PDF + X-API-Key Header
?
Tenant Resolution Middleware ? API-Key ? Tenant aus DB (Redis Cache)
?
DocumentOperator API (Minimal API Endpoint)
?
[FluentValidation] ? [MediatR Handler] ? [DevExpress Service] ? [Ergebnis]
?
[HTTP Response] - JSON mit verarbeitetem PDF (Base64)
```
**Asynchroner Flow (große Operationen > 5 Sekunden):**
```
Client Application
?
[HTTP POST /api/v1/documents/concatenate] - Große Operation
?
[Returns: { "jobId": "abc123", "status": "Pending" }] - Sofort
?
Background Worker (IHostedService) ? In-Memory Queue ? Verarbeitung
?
Client Poll: [GET /api/v1/jobs/abc123] ? { "status": "Processing", "progress": 45% }
?
Client Poll: [GET /api/v1/jobs/abc123] ? { "status": "Success", "resultUrl": "/download/xyz" }
```
**Typischer Ablauf (Synchron):**
1. Client sendet PDF als Base64 in JSON + API-Key Header
2. Tenant Resolution Middleware validiert API-Key (DB-Lookup mit Redis Cache)
3. API validiert Input (FluentValidation in MediatR Pipeline)
4. Handler konvertiert PDF ? Byte-Array
5. DevExpress Service führt Operation durch (mit Polly Retry/Circuit Breaker)
6. Ergebnis wird in lokalem Temp-Ordner gespeichert
7. Ergebnis wird als Base64 zurückgegeben (oder Download-Link)
---
## ??? ARCHITECTURE & DESIGN DECISIONS
### Clean Architecture (Pragmatisch!)
Wir verwenden **Clean Architecture** mit 4 Layers - **ABER: pragmatisch, nicht dogmatisch!**
```
???????????????????????????????????????
? API Layer (Endpoints) ? ? HTTP Entry Point
???????????????????????????????????????
? Application Layer (Use Cases) ? ? MediatR Handlers, DTOs
???????????????????????????????????????
? Infrastructure Layer (Tech Stack) ? ? DevExpress, File I/O
???????????????????????????????????????
? Domain Layer (MINIMAL!) ? ? Nur Enums + Value Objects
???????????????????????????????????????
```
#### Dependency Rule
**Abhängigkeiten zeigen immer nach innen:**
```
API ? Application ? Domain
API ? Infrastructure ? Domain
Infrastructure ? Application (für Interfaces)
Domain ? NICHTS! (No External Dependencies)
Application ? NUR Domain
```
**Warum Clean Architecture?**
- ? Testbarkeit (Application Layer kann Services mocken)
- ? Austauschbarkeit (DevExpress ? anderes PDF-Lib ohne Application zu ändern)
- ? Separation of Concerns (jede Schicht hat klare Verantwortung)
**ABER:**
- ? Kein Overengineering (nur was wir wirklich brauchen!)
- ? Keine spekulativen Abstraktionen (erst wenn 2. Use Case es braucht)
- ? Keine unnötigen Klassen (YAGNI - You Ain't Gonna Need It)
---
### Domain Layer - Warum so minimal?
**Was wir NICHT haben:**
- ? Keine Datenbank / EF Core
- ? Keine komplexen Entities mit Business-Logik
- ? Keine Aggregate Roots, Repositories, etc.
**Was wir SIND:**
- ? Ein **Service** (nicht eine Domain-lastige Business-Anwendung)
- ? PDF-Operationen = technische Operationen (nicht fachliche Geschäftslogik)
- ? Daten fließen durch (Input ? Verarbeitung ? Output)
**Deshalb: Domain Layer minimal!**
**Was bleibt in Domain:**
1. **Enums** (DocumentOperationType, ProcessingStatus)
- Pure Business-Konzepte
- Technologie-unabhängig
- Wiederverwendbar über alle Layer
2. **Value Objects** (Base64String, TenantId, PdfMetadata)
- Typsicherheit (Base64String statt string)
- Selbst-validierend (Fehler werfen im Constructor)
- Immutable (keine Änderungen nach Erstellung)
3. **Domain Exceptions** (DomainValidationException, PdfProcessingException, etc.)
- Für fachliche Fehler
- Exception Middleware mapped zu HTTP Status Codes
**Was wir NICHT in Domain haben:**
- ? Domain Models (PdfDocument, DocumentAttachment) ? DTOs in Application reichen!
- ? Constants (ErrorCodes) ? erst wenn wirklich mehrfach gebraucht (YAGNI)
- ? Services (? Infrastructure)
**Fazit:**
- Domain = **so viel wie nötig, so wenig wie möglich**
- Wenn wir später merken "das fehlt" ? dann erst hinzufügen (iterativ!)
---
### CQRS with MediatR
**Pattern:** Command Query Responsibility Segregation
**Warum MediatR?**
- ? Klare Trennung: 1 Command/Query = 1 Handler = 1 Verantwortung
- ? Testbarkeit (Handler kann isoliert getestet werden)
- ? Pipeline Behaviors (Validation, Logging zentral)
- ? Kein aufgeblähter Service mit 20 Methoden
**CQRS in unserem Kontext:**
- **Command:** Ändert Daten (ProcessDocument, ApplyStamp, etc.)
- **Query:** Liest Daten (ValidatePdf ? gibt nur Metadata zurück)
**Beispiel:**
```csharp
// Query (Read-Only)
public record ValidatePdfQuery(Base64String PdfContent) : IRequest<PdfMetadata>;
// Handler
public class ValidatePdfHandler : IRequestHandler<ValidatePdfQuery, PdfMetadata>
{
private readonly IPdfProcessor _processor;
public async Task<PdfMetadata> Handle(ValidatePdfQuery query, CancellationToken ct)
{
byte[] bytes = query.PdfContent.ToByteArray();
var metadata = await _processor.ValidateAsync(bytes);
return metadata;
}
}
```
**Warum Value Objects in Query/Command?**
- ? Typsicherheit (Base64String vs string)
- ? Validierung bereits beim Erstellen der Query (nicht im Handler)
- ? Handler bleibt schlank (keine Validierungs-Boilerplate)
---
### Vertical Slice Architecture
**Statt Horizontal Layers** (Commands/, Handlers/, Validators/):
```
? Horizontal (Schlecht für Wartung):
Application/
??? Commands/
? ??? ValidatePdfCommand.cs
? ??? ProcessDocumentCommand.cs
??? Handlers/
? ??? ValidatePdfHandler.cs
? ??? ProcessDocumentHandler.cs
??? Validators/
??? ValidatePdfValidator.cs
??? ProcessDocumentValidator.cs
```
**Nutzen wir Vertical Slices** (pro Feature alles zusammen):
```
? Vertical (Gut für Wartung):
Features/
??? ValidatePdf/
? ??? ValidatePdfQuery.cs
? ??? ValidatePdfHandler.cs
? ??? ValidatePdfValidator.cs
??? ProcessDocument/
??? ProcessDocumentCommand.cs
??? ProcessDocumentHandler.cs
??? ProcessDocumentValidator.cs
```
**Vorteile:**
- ? Zusammengehöriger Code ist zusammen (Cohesion)
- ? Einfacher zu finden ("Wo ist ValidatePdf?" ? ein Ordner!)
- ? Einfacher zu ändern (alle Dateien im gleichen Ordner)
- ? Weniger Merge-Konflikte im Team
---
### Exception-based Error Handling
**Entscheidung:** Keine Result Pattern Library (Ardalis.Result entfernt)
**Stattdessen:**
1. **FluentValidation** für Input-Validierung (DTO-Ebene)
2. **Domain Exceptions** für fachliche Fehler
3. **Zentrale Exception Handling Middleware** im API Layer
**Warum Exception-basiert?**
- ? Einfacherer Code (kein `if (result.IsSuccess)` überall)
- ? Weniger Boilerplate (kein Result<T> Wrapping)
- ? Standard .NET Exception-Flow (jeder kennt es)
- ? Zentrales Error Handling = wartbar an **einer** Stelle
- ? Ein Package weniger (keine Extra-Lib)
**Flow:**
```
HTTP Request
?
FluentValidation (MediatR ValidationBehavior)
? Bei Fehler: ValidationException ? Middleware ? HTTP 400
?
Handler
? Bei Fehler: DomainException ? Middleware ? HTTP 400/404/500
?
Middleware (Exception Handler)
? Mappt Exception Type ? HTTP Status Code
? Loggt Exception (Serilog)
? Gibt Problem Details (RFC 7807) zurück
?
HTTP Response (JSON)
```
**Exception Types:**
- `FluentValidation.ValidationException` ? HTTP 400 (Bad Request)
- `DomainValidationException` ? HTTP 400 (Bad Request)
- `NotFoundException` ? HTTP 404 (Not Found)
- `PdfProcessingException` ? HTTP 500 (Internal Server Error)
- `Exception` (Catch-All) ? HTTP 500
**Warum zentral?**
- Alle Fehler an **einer** Stelle behandelt
- Konsistente Error-Responses (Problem Details Format)
- Handler bleiben schlank (kein Try/Catch in jedem Handler)
- Logging zentral (Serilog)
---
### Minimal APIs (statt Controllers)
**Warum Minimal APIs?**
- ? .NET 8 Best Practice (Microsoft empfiehlt es)
- ? Weniger Boilerplate (keine Controller-Klassen)
- ? Direkte Endpoint-Definition (funktionaler Stil)
- ? Swagger funktioniert 1:1 (WithOpenApi())
- ? Bessere Performance (weniger Abstraktion)
**Beispiel:**
```csharp
app.MapPost("/api/v1/documents/validate", async (
ValidatePdfRequest request,
IMediator mediator,
CancellationToken ct) =>
{
// DTO ? Query (Value Objects erstellen)
var query = new ValidatePdfQuery(
Base64String.Create(request.Base64Pdf)
);
// MediatR Handler aufrufen
var result = await mediator.Send(query, ct);
// HTTP 200 + JSON Response
return Results.Ok(result);
})
.WithName("ValidatePdf")
.WithTags("Documents")
.WithOpenApi();
```
**Flow:**
1. HTTP Request kommt rein
2. ASP.NET Core deserialisiert JSON ? DTO
3. Endpoint ruft MediatR auf
4. MediatR Pipeline: Validation ? Handler ? Response
5. Endpoint gibt Result zurück (Results.Ok())
---
### Multi-Tenancy via API-Keys (Database-based)
**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`
- API-Keys werden in **Datenbank** gespeichert (SQLite für Einfachheit)
- Redis Cache für schnelle API-Key Lookups (Performance!)
- Middleware resolved API-Key ? Tenant-Context
- Tenant-spezifische Einstellungen (Logo für Stamps, Zertifikat, etc.)
**Warum Database-based (statt appsettings.json)?**
- ? **Skalierbar:** Neue Tenants ohne Neustart hinzufügen
- ? **Security:** API-Key Rotation möglich (gehashed in DB!)
- ? **Audit-Log:** Wer hat wann was aufgerufen?
- ? **Rate-Limiting:** Pro Tenant konfigurierbar (Redis Sliding Window)
- ? **Tenant-Management:** CRUD-API für API-Keys (Admin-Endpoint)
**Warum SQLite (statt SQL Server)?**
- ? Einfache Deployment (keine separate DB-Server)
- ? Wenige Daten (nur Tenant-Tabelle + Settings)
- ? Migrations-Support (EF Core)
- ? Production-ready (für < 10.000 Requests/Sekunde ausreichend)
**Flow:**
```
HTTP Request mit Header "X-API-Key: abc123"
?
TenantResolutionMiddleware
?
Redis Cache Lookup (Key: "tenant:abc123")
? Cache Hit: Tenant-Info geladen (1ms)
? Cache Miss: DB Lookup ? Redis Cache befüllen (TTL: 1 Stunde)
?
API-Key Hash validieren (BCrypt)
?
Tenant.IsActive prüfen (inaktive Tenants ? HTTP 403)
?
ITenantContext setzen (Scoped Service)
?
Handler nutzt ITenantContext.TenantId
```
**Datenbank-Schema:**
```csharp
// Tenant-Tabelle (EF Core Entity)
public class Tenant
{
public Guid Id { get; set; }
public string Name { get; set; } // "Customer A"
public string ApiKeyHash { get; set; } // BCrypt Hash
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastUsedAt { get; set; }
// Navigation
public TenantSettings Settings { get; set; }
}
// Tenant-Settings (1:1 Beziehung)
public class TenantSettings
{
public Guid Id { get; set; }
public Guid TenantId { get; set; }
public string LogoFilePath { get; set; } // Lokaler Dateipfad (z.B. "logos/tenant-a/stamp.png")
public string CertificateFilePath { get; set; } // Lokaler Dateipfad (z.B. "certs/tenant-a/cert.pfx")
public int RateLimitPerMinute { get; set; } // Rate-Limiting
// Navigation
public Tenant Tenant { get; set; }
}
```
**Beispiel - Tenant-spezifischer Stamp:**
```csharp
public class ApplyStampHandler : IRequestHandler<ApplyStampCommand, byte[]>
{
private readonly ITenantContext _tenantContext;
private readonly IFileStorage _fileStorage; // Lokaler File Storage
public async Task<byte[]> Handle(ApplyStampCommand command, CancellationToken ct)
{
// Tenant-spezifisches Logo aus DB Settings laden
var logoPath = _tenantContext.CurrentTenant.Settings.LogoFilePath;
// Logo aus lokalem Dateisystem laden
var logoBytes = await _fileStorage.GetAsync(logoPath);
// Stamp mit Logo anwenden
// ...
}
}
```
**Vorteile:**
- ? Neue Tenants via Admin-API hinzufügen (ohne Neustart!)
- ? API-Key Rotation (alten Key invalidieren, neuen generieren)
- ? Audit-Log (LastUsedAt pro Tenant tracken)
- ? Rate-Limiting pro Tenant (Redis Counter)
---
## ??? TECHNOLOGY STACK
### Core Framework
| Technology | Version | Purpose |
|------------|---------|---------|
| **.NET** | 8.0 | Runtime & Framework |
| **ASP.NET Core** | 8.0 | Web API |
| **C#** | 12 | Language (Primary Constructors, Record Types) |
---
### NuGet 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.Sinks.Seq** | 8.0.0 | **NEU:** Log-Browsing UI (Development) |
| **Serilog.Enrichers.Environment** | 3.0.1 | Log-Enrichment (MachineName, etc.) |
| **Serilog.Enrichers.CorrelationId** | 3.0.1 | **NEU:** Correlation IDs für Request-Tracking |
| **Asp.Versioning.Http** | 8.1.1 | API Versioning (/api/v1/, /api/v2/) |
| **Microsoft.Extensions.Caching.StackExchangeRedis** | 8.0.28 | Redis Cache (Tenant-Lookups, Rate-Limiting) |
#### Application Layer
| Package | Version | Purpose |
|---------|---------|---------|
| **MediatR** | 14.1.0 | CQRS Pattern Implementation |
| **FluentValidation** | 12.1.1 | Input Validation (DTOs) |
| **FluentValidation.DependencyInjectionExtensions** | 12.1.1 | DI Integration |
#### Infrastructure Layer
| Package | Version | Purpose |
|---------|---------|---------|
| **DevExpress.Pdf.Core** | 25.2.8 | PDF-Operationen (Merge, Extract, Sign, etc.) |
| **DevExpress Universal License** | ? Verfügbar | **Vollzugriff auf alle DevExpress Bibliotheken** |
| **Microsoft.EntityFrameworkCore** | 8.0.0 | **NEU:** ORM für Tenant-Datenbank |
| **Microsoft.EntityFrameworkCore.Sqlite** | 8.0.0 | **NEU:** SQLite Provider (Tenant-DB) |
| **Microsoft.EntityFrameworkCore.Tools** | 8.0.0 | **NEU:** Migrations-Support |
| **Polly** | 8.5.0 | **NEU:** Resilience (Retry, Circuit Breaker, Timeout) |
| **BCrypt.Net-Next** | 4.0.3 | **NEU:** API-Key Hashing (Security) |
| **Microsoft.Extensions.Options.ConfigurationExtensions** | 8.0.0 | Options Pattern |
**Hinweis zur DevExpress Lizenz:**
- ? Universal License vorhanden - wir können **ALLE** DevExpress Pakete nutzen
- Neben `DevExpress.Pdf.Core` können wir auch weitere Pakete integrieren:
- `DevExpress.Office.Core` (Word, Excel)
- `DevExpress.Document.Processor` (erweiterte Dokumenten-Verarbeitung)
- `DevExpress.Blazor` (falls UI später benötigt wird)
- Alle weiteren DevExpress Produkte nach Bedarf
**Warum diese neuen Pakete?**
- **EF Core + SQLite:** Tenant-Datenbank (API-Keys, Settings) - skalierbar ohne SQL Server
- **Polly:** Resilience für DevExpress Calls (Retry bei Transient Errors, Circuit Breaker)
- **BCrypt:** Sichere API-Key Hashes (NICHT Klartext in DB!)
- **Seq:** Log-Browsing UI für Development
- **Correlation IDs:** Request-Tracking über alle Logs (Debugging leichter)
#### Domain Layer
| Package | Version | Purpose |
|---------|---------|---------|
| - | - | **Keine Dependencies!** (Clean Architecture) |
#### Tests (neu!)
| Package | Version | Purpose |
|---------|---------|---------|
| **xUnit** | 2.9.3 | Test Framework |
| **FluentAssertions** | 7.0.0 | Assertions (result.Should().Be(expected)) |
| **Moq** | 4.20.72 | Mocking (für Services) |
| **Microsoft.NET.Test.Sdk** | 17.11.1 | Test SDK |
| **xunit.runner.visualstudio** | 2.8.2 | Visual Studio Test Runner |
---
## ?? PROJECT STRUCTURE
### Solution Overview
```
DocumentOperator/
??? DocumentOperator.API/ ? HTTP Entry Point
??? DocumentOperator.Application/ ? Use Cases (MediatR Handlers)
??? DocumentOperator.Infrastructure/ ? Technical Implementations
??? DocumentOperator.Domain/ ? Business Logic (MINIMAL!)
??? DocumentOperator.Tests/ ? Unit & Integration Tests (NEU!)
??? ROADMAP.md ? This file
```
---
### ?? API Layer (DocumentOperator.API)
**Purpose:** HTTP Entry Point, Routing, Middleware
**References:**
- ? Application
- ? Infrastructure
- ? Domain
**Folder Structure:**
```
DocumentOperator.API/
??? Endpoints/
? ??? v1/
? ??? DocumentEndpoints.cs ? Minimal API Endpoints
??? Middleware/
? ??? ExceptionHandlingMiddleware.cs ? Zentrale Exception Handling ?
??? Configuration/
? ??? SwaggerConfiguration.cs ? Swagger Setup (API-Key Support)
??? appsettings.json ? Base Configuration
??? appsettings.Development.json ? Dev Overrides
??? Program.cs ? Application Entry Point
```
**Was gehört hierher:**
- ? HTTP Routing (Minimal APIs)
- ? Middleware (Exception, Logging)
- ? Swagger Configuration
- ? Dependency Injection Setup
- ? appsettings.json
**Was NICHT hierher gehört:**
- ? Business Logic (? Application)
- ? PDF-Verarbeitung (? Infrastructure)
- ? Validierung (? Application: FluentValidation)
---
### ?? Application Layer (DocumentOperator.Application)
**Purpose:** Use Cases, Business Logic Orchestration
**References:**
- ? Domain (ONLY!)
**Folder Structure:**
```
DocumentOperator.Application/
??? Features/ ? Vertical Slices ?
? ??? Documents/
? ? ??? ValidatePdf/
? ? ? ??? ValidatePdfQuery.cs
? ? ? ??? ValidatePdfHandler.cs
? ? ? ??? ValidatePdfValidator.cs
? ? ??? ExtractAttachments/
? ? ? ??? ExtractAttachmentsCommand.cs
? ? ? ??? ExtractAttachmentsHandler.cs
? ? ? ??? ExtractAttachmentsValidator.cs
? ? ??? ... (weitere Features iterativ)
? ??? Jobs/ ? **NEU:** Async Processing Features
? ??? GetJobStatus/
? ? ??? GetJobStatusQuery.cs
? ? ??? GetJobStatusHandler.cs
? ??? SubmitConcatenateJob/
? ??? SubmitConcatenateJobCommand.cs
? ??? SubmitConcatenateJobHandler.cs
??? Common/
? ??? Interfaces/ ? Abstractions für Infrastructure
? ? ??? IPdfProcessor.cs
? ? ??? IFileStorage.cs ? **NEU:** File Storage Abstraction
? ? ??? IJobQueue.cs ? **NEU:** Queue Abstraction (In-Memory Queue)
? ? ??? ITenantRepository.cs ? **NEU:** Tenant-DB Abstraction
? ??? Behaviors/ ? MediatR Pipeline Behaviors
? ? ??? ValidationBehavior.cs ? FluentValidation Integration
? ? ??? LoggingBehavior.cs ? **NEU:** Structured Logging mit Correlation IDs
? ??? DTOs/ ? Data Transfer Objects (API Contracts)
? ? ??? ValidatePdfRequest.cs
? ? ??? ValidatePdfResponse.cs
? ? ??? JobStatusResponse.cs ? **NEU:** Async Job Status
? ??? Models/ ? **NEU:** Shared Models
? ??? TenantContext.cs ? Tenant-Info (TenantId, Name, Settings)
??? DependencyInjection.cs ? Service Registration
```
**Was gehört hierher:**
- ? MediatR Commands & Queries (pro Feature)
- ? Handlers (orchestrieren Domain + Infrastructure)
- ? FluentValidation Validators
- ? DTOs (API Contracts)
- ? Interfaces für Infrastructure (Dependency Inversion!)
- ? Pipeline Behaviors (Validation, Logging)
**Was NICHT hierher gehört:**
- ? DevExpress-spezifischer Code (? Infrastructure)
- ? File I/O (? Infrastructure)
- ? HTTP-spezifisches (? API)
**Warum keine Infrastructure-Referenz?**
- Application kennt nur **Interfaces** (`IPdfProcessor`)
- Infrastructure **implementiert** die Interfaces (`DevExpressPdfProcessor`)
- API injiziert die Implementierung via DI
- ? Application bleibt technologie-unabhängig!
---
### ?? Infrastructure Layer (DocumentOperator.Infrastructure)
**Purpose:** Technische Implementierungen
**References:**
- ? Application (für Interfaces)
- ? Domain
**Folder Structure:**
```
DocumentOperator.Infrastructure/
??? Services/
? ??? PdfProcessing/
? ? ??? DevExpressPdfProcessor.cs ? IPdfProcessor Implementation (mit Polly Resilience)
? ??? FileStorage/
? ? ??? LocalFileStorage.cs ? **NEU:** IFileStorage Implementation (lokaler Temp-Ordner)
? ??? Queue/
? ??? InMemoryJobQueue.cs ? **NEU:** IJobQueue Implementation (In-Memory Queue)
??? Data/
? ??? TenantDbContext.cs ? **NEU:** EF Core DbContext (Tenant-DB)
? ??? Entities/
? ? ??? Tenant.cs ? **NEU:** Tenant Entity
? ? ??? TenantSettings.cs ? **NEU:** TenantSettings Entity
? ??? Repositories/
? ? ??? TenantRepository.cs ? **NEU:** ITenantRepository Implementation
? ??? Migrations/ ? **NEU:** EF Core Migrations
??? BackgroundServices/
? ??? JobProcessorService.cs ? **NEU:** IHostedService für Async Job Processing
? ??? TempFileCleanupService.cs ? **NEU:** IHostedService für Temp-File Cleanup (täglich)
??? Configuration/
? ??? DocumentOperatorSettings.cs ? Options Pattern Class
? ??? FileStorageSettings.cs ? **NEU:** File Storage Configuration (Temp-Ordner Pfad)
? ??? RedisSettings.cs ? **NEU:** Redis Cache Configuration
??? DependencyInjection.cs ? Service Registration
```
**Was gehört hierher:**
- ? DevExpress Integration (mit Polly Resilience!)
- ? **File Storage:** Lokaler Temp-Ordner (IFileStorage Abstraction)
- ? **Queue:** In-Memory Queue für Async Processing
- ? **Datenbank:** EF Core + SQLite (Tenant-Management)
- ? **Background Services:** Job Processing, Temp-File Cleanup
- ? Options Pattern Classes (Settings)
**Was NICHT hierher gehört:**
- ? Business Logic (? Application)
- ? HTTP Handling (? API)
---
### ??? Domain Layer (DocumentOperator.Domain) - MINIMAL!
**Purpose:** Business Rules (nur was wirklich gebraucht wird!)
**References:**
- ? **KEINE!** (wichtigste Clean Architecture Regel)
**Folder Structure:**
```
DocumentOperator.Domain/
??? ValueObjects/ ? Immutable, selbst-validierend
? ??? Base64String.cs
? ??? TenantId.cs
? ??? PdfMetadata.cs
? ??? JobId.cs ? **NEU:** Job-ID für Async Processing
??? Enums/
? ??? DocumentOperationType.cs
? ??? ProcessingStatus.cs ? (Wird jetzt für Async Jobs genutzt!)
??? Exceptions/ ? Domain-spezifische Exceptions
??? DomainException.cs
??? DomainValidationException.cs
??? NotFoundException.cs
??? PdfProcessingException.cs
```
**Was gehört hierher:**
- ? Value Objects (Base64String, TenantId, PdfMetadata, JobId)
- ? Enums (DocumentOperationType, ProcessingStatus)
- ? Domain Exceptions
**Wichtig:**
- Domain bleibt **MINIMAL** - keine EF Core Entities hier!
- Tenant, TenantSettings sind **Infrastructure** Entities (Data/Entities/)
- Domain kennt nur Value Objects (keine Navigation Properties, kein EF Core)
**Was NICHT hierher gehört:**
- ? Domain Models (PdfDocument, etc.) ? YAGNI! DTOs reichen!
- ? Constants (ErrorCodes) ? erst wenn mehrfach gebraucht
- ? Services (? Infrastructure)
- ? MediatR (? Application)
- ? JEGLICHE externe Library!
---
### ?? Tests Layer (DocumentOperator.Tests) - NEU!
**Purpose:** Unit & Integration Tests
**References:**
- ? Alle Projekte (API, Application, Infrastructure, Domain)
**Folder Structure:**
```
DocumentOperator.Tests/
??? Unit/
? ??? Application/
? ? ??? Features/
? ? ??? ValidatePdf/
? ? ??? ValidatePdfHandlerTests.cs
? ??? Infrastructure/
? ? ??? Services/
? ? ??? DevExpressPdfProcessorTests.cs
? ??? Domain/
? ??? ValueObjects/
? ??? Base64StringTests.cs
??? Integration/
??? API/
??? ValidatePdfEndpointTests.cs
```
**Test-Strategie:**
- ? TDD (Test-Driven Development)
- ? Unit Tests für Handler (Application Layer)
- ? Unit Tests für Services (Infrastructure Layer)
- ? Unit Tests für Value Objects (Domain Layer)
- ? Integration Tests für Endpoints (API Layer)
---
## ?? DEVELOPMENT PHILOSOPHY
### Pragmatisch, nicht dogmatisch!
**Prinzipien:**
1. **YAGNI (You Ain't Gonna Need It)**
- ? Keine spekulativen Abstraktionen
- ? Keine Klassen "für später"
- ? Erst wenn 2. Use Case es braucht ? dann Abstrahieren
2. **KISS (Keep It Simple, Stupid)**
- ? Kein Overengineering
- ? Keine unnötigen Design Patterns
- ? Einfachster Code der funktioniert
3. **Clean Architecture JA, aber pragmatisch**
- ? Dependency Rule einhalten (wichtig!)
- ? Separation of Concerns (wichtig!)
- ? ABER: Nur Abstraktionen die wir wirklich brauchen
4. **Test-Driven Development (TDD)**
- ? Tests schreiben **bevor** Code (Red ? Green ? Refactor)
- ? Tests als Dokumentation (wie wird es genutzt?)
- ? Tests als Safety Net (Refactoring ohne Angst)
5. **Outside-In Development**
- ? Von außen nach innen bauen (API ? Service ? Domain)
- ? Wir sehen sofort was funktioniert (kein "spekulatives" Code)
- ? Feedback-Loop schneller
**Konkret für unser Projekt:**
- Domain Layer **minimal** (nur Enums + Value Objects + Exceptions)
- Keine Domain Models (DTOs in Application reichen!)
- Keine Constants (erst wenn mehrfach gebraucht)
- Iterativ entwickeln (Feature für Feature)
- TDD (Test ? Code ? Refactor)
---
## ??? DEVELOPMENT ROADMAP
### ? PHASE 1: Foundation - **COMPLETED**
**Bereits erledigt:**
- [x] Solution erstellt (4 Projekte)
- [x] Dependencies korrekt (Clean Architecture Dependency Rule)
- [x] NuGet Packages installiert
- [x] Folder-Struktur erstellt
- [x] appsettings.json konfiguriert
- [x] Options Pattern Classes erstellt
- [x] Serilog Setup (Program.cs)
---
### ? PHASE 2: Domain Layer (Minimal) - **COMPLETED**
**Ziel:** Nur was wirklich gebraucht wird!
**Status:** ? **Alle Steps abgeschlossen!**
---
#### ? Step 2.1: Domain Exceptions erstellen - **COMPLETED**
**Bereits 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/`
---
#### ? Step 2.2: Enums erstellen - **COMPLETED**
**Aufgabe:** Aufzählungen für Business-Konzepte
**Warum JETZT (vor Value Objects)?**
- Enums haben keine Dependencies
- Werden in Value Objects gebraucht (z.B. PdfMetadata)
- Schneller Erfolg (5 Minuten Arbeit)
**Was du tun wirst:**
1. **DocumentOperationType.cs** erstellen
- **Wo:** `Domain/Models/Enums/DocumentOperationType.cs`
- **Inhalt:**
```csharp
namespace DocumentOperator.Domain.Models.Enums;
public enum DocumentOperationType
{
Validate,
ExtractAttachments,
Concatenate,
ApplyStamp,
EmbedCertificate
}
```
- **Warum:** Definiert welche Operationen unser Service kann
- **Wo gebraucht:** Später in Commands/DTOs
2. **ProcessingStatus.cs** erstellen
- **Wo:** `Domain/Models/Enums/ProcessingStatus.cs`
- **Inhalt:**
```csharp
namespace DocumentOperator.Domain.Models.Enums;
public enum ProcessingStatus
{
Pending,
Processing,
Success,
Failed
}
```
- **Warum:** Status für asynchrone Operationen (später: Queue)
- **Wo gebraucht:** Response DTOs
**Nach diesem Step:**
- Ich prüfe deine Dateien
- Wir haken Step 2.2 ab in ROADMAP.md
- Weiter zu Step 2.3 (Value Objects)
**Status:** ? **COMPLETED** (17.01.2025)
- ? DocumentOperationType.cs erstellt
- ? ProcessingStatus.cs erstellt
- ? Build erfolgreich
---
#### ? Step 2.3: Value Objects erstellen - **COMPLETED**
**Aufgabe:** Typsichere, selbst-validierende Wert-Objekte
**Warum Value Objects?**
- ? Typsicherheit: `Base64String` statt `string`
- ? Validierung an **einer** Stelle (Constructor)
- ? Immutable (keine Änderungen nach Erstellung)
- ? Wiederverwendbar (in Domain, Application, Infrastructure)
**Was du erstellt hast:**
1. **Base64String.cs** ?
- Factory Method: `Create(string value)`
- Validierung: Gültiges Base64-Format
- Konvertierung: `ToByteArray()`, `FromByteArray(byte[])`
- 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, AttachmentCount
- Computed Property: `FileSizeMB`
- Keine Validierung (nur Daten-Container)
**Wo:** `Domain/Models/ValueObjects/`
**Status:** ? **COMPLETED** (17.01.2025)
- ? Base64String.cs erstellt (sealed, Factory Methods, Validierung, Equality)
- ? TenantId.cs erstellt (sealed, Normalisierung, Validierung, Equality)
- ? PdfMetadata.cs erstellt (sealed, Computed Property, ToString())
- ? Build erfolgreich
**?? Phase 2 (Domain Layer) komplett abgeschlossen!**
---
### ? PHASE 3: Infrastructure Layer (Outside-In!) - **NEXT**
**Ziel:** DevExpress Services implementieren (wir sehen **echten** Code!)
---
#### ? Step 3.1: IPdfProcessor Interface erstellen - **COMPLETED**
**Aufgabe:** Abstraction für PDF-Operationen
**Was du erstellt hast:**
- **Wo:** `Application/Common/Interfaces/IPdfProcessor.cs` ?
- **Inhalt:**
```csharp
using DocumentOperator.Domain.Models.ValueObjects;
namespace DocumentOperator.Application.Common.Interfaces;
public interface IPdfProcessor
{
Task<PdfMetadata> ValidateAsync(byte[] pdfBytes);
}
```
**Warum Interface ERST?**
- Application kennt nur Interface (Dependency Inversion)
- Infrastructure implementiert
- TDD: Test ? Interface ? Implementation
**Status:** ? **COMPLETED** (17.01.2025)
- ? Interface erstellt mit XML Comments
- ? Using Statement korrekt
- ? Namespace korrekt
- ? Build erfolgreich
---
#### ?? Step 3.2: DevExpressPdfProcessor implementieren (mit TDD!) - **NEXT**
**Aufgabe:** DevExpress Integration
**Flow:**
1. **Test schreiben** (Red)
```csharp
[Fact]
public async Task ValidateAsync_ValidPdf_ReturnMetadata()
{
// Arrange
var processor = new DevExpressPdfProcessor();
byte[] validPdf = CreateDummyPdf();
// Act
var metadata = await processor.ValidateAsync(validPdf);
// Assert
metadata.PageCount.Should().BeGreaterThan(0);
}
```
2. **Implementation schreiben** (Green)
```csharp
public class DevExpressPdfProcessor : IPdfProcessor
{
public async Task<PdfMetadata> ValidateAsync(byte[] pdfBytes)
{
using var processor = new PdfDocumentProcessor();
processor.LoadDocument(pdfBytes);
return new PdfMetadata(
PageCount: processor.Document.Pages.Count,
FileSizeBytes: pdfBytes.Length,
// ...
);
}
}
```
3. **Test grün machen**
4. **Refactoring** (falls nötig)
**Wo:**
- Test: `Tests/Unit/Infrastructure/Services/DevExpressPdfProcessorTests.cs`
- Code: `Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs`
**Nach diesem Step:**
- Wir haben **echten** Code der mit DevExpress arbeitet!
- Wir wissen welche Exceptions geworfen werden können
- Wir können Exception Middleware bauen
---
### ? PHASE 4: Application Layer (erste Feature)
**Ziel:** ValidatePdf Feature komplett (Query ? Handler ? Validator)
---
#### ? Step 4.1: MediatR Setup
**Aufgabe:** MediatR + FluentValidation + ValidationBehavior
**Was du erstellen wirst:**
1. `DependencyInjection.cs` (Application Layer)
2. `ValidationBehavior.cs` (MediatR Pipeline)
**Warum jetzt?**
- Wir brauchen MediatR für Handler
- ValidationBehavior = zentrale FluentValidation Ausführung
---
#### ? Step 4.2: ValidatePdf Feature (mit TDD!)
**Aufgabe:** Erste komplette Feature-Implementierung
**Was du erstellen wirst:**
1. **ValidatePdfQuery.cs**
```csharp
public record ValidatePdfQuery(Base64String PdfContent) : IRequest<PdfMetadata>;
```
2. **ValidatePdfHandler.cs**
```csharp
public class ValidatePdfHandler : IRequestHandler<ValidatePdfQuery, PdfMetadata>
{
private readonly IPdfProcessor _processor;
public async Task<PdfMetadata> Handle(ValidatePdfQuery query, CancellationToken ct)
{
byte[] bytes = query.PdfContent.ToByteArray();
return await _processor.ValidateAsync(bytes);
}
}
```
3. **ValidatePdfValidator.cs** (FluentValidation)
```csharp
public class ValidatePdfValidator : AbstractValidator<ValidatePdfQuery>
{
public ValidatePdfValidator()
{
RuleFor(x => x.PdfContent).NotNull();
}
}
```
4. **ValidatePdfHandlerTests.cs** (Unit Test)
**Wo:** `Application/Features/Documents/ValidatePdf/`
**Flow:**
```
DTO ? Query (Value Objects) ? ValidationBehavior (FluentValidation)
? Handler ? IPdfProcessor ? PdfMetadata
```
---
### ? PHASE 5: API Layer
**Ziel:** HTTP Endpoint + Exception Middleware
---
#### ? Step 5.1: Exception Handling Middleware
**Aufgabe:** Zentrale Exception ? HTTP Response Mapping
**Was du erstellen wirst:**
- **Wo:** `API/Middleware/ExceptionHandlingMiddleware.cs`
- **Inhalt:**
```csharp
public class ExceptionHandlingMiddleware
{
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (DomainValidationException ex)
{
await HandleDomainValidationExceptionAsync(context, ex);
}
catch (PdfProcessingException ex)
{
await HandlePdfProcessingExceptionAsync(context, ex);
}
// ... weitere Exceptions
}
private static Task HandleDomainValidationExceptionAsync(...)
{
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 = 400,
Detail = ex.Message
};
return context.Response.WriteAsJsonAsync(problemDetails);
}
}
```
**Warum jetzt?**
- Wir kennen jetzt alle Exceptions (aus Infrastructure Step)
- Wir können sie zu HTTP Status Codes mappen
---
#### ? Step 5.2: Minimal API Endpoint
**Aufgabe:** HTTP Endpoint für ValidatePdf
**Was du erstellen wirst:**
- **Wo:** `API/Endpoints/v1/DocumentEndpoints.cs`
- **Inhalt:**
```csharp
public static class DocumentEndpoints
{
public static void MapDocumentEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/documents")
.WithTags("Documents")
.WithOpenApi();
group.MapPost("/validate", ValidatePdf);
}
private static async Task<IResult> ValidatePdf(
ValidatePdfRequest request,
IMediator mediator,
CancellationToken ct)
{
var query = new ValidatePdfQuery(
Base64String.Create(request.Base64Pdf)
);
var result = await mediator.Send(query, ct);
return Results.Ok(result);
}
}
```
**DTOs:**
- `ValidatePdfRequest` (Input)
- `ValidatePdfResponse` (Output) ? oder direkt PdfMetadata?
**In Program.cs registrieren:**
```csharp
app.MapDocumentEndpoints();
```
---
#### ? Step 5.3: Integration Test
**Aufgabe:** End-to-End Test (HTTP ? Handler ? Service)
**Was du erstellen wirst:**
- **Wo:** `Tests/Integration/API/ValidatePdfEndpointTests.cs`
- **Inhalt:**
```csharp
public class ValidatePdfEndpointTests : IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task POST_ValidatePdf_ValidPdf_Returns200()
{
// Arrange
var client = _factory.CreateClient();
var request = new ValidatePdfRequest(Base64Pdf: "...");
// Act
var response = await client.PostAsJsonAsync("/api/v1/documents/validate", request);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var metadata = await response.Content.ReadFromJsonAsync<PdfMetadata>();
metadata.PageCount.Should().BeGreaterThan(0);
}
}
```
**Warum Integration Test?**
- Testet kompletten Flow (HTTP ? MediatR ? Service ? Response)
- Testet Exception Middleware
- Testet Swagger/OpenAPI
---
### ? PHASE 5.5: Health Checks & Resilience - **NEW!**
**Ziel:** Production-Ready Features FRÜH implementieren
**Warum JETZT (nicht Phase 9)?**
- Health Checks = **Pflicht** für Production (Load Balancer, Kubernetes)
- Resilience (Polly) = **Pflicht** für externe Dependencies (DevExpress)
---
#### ? Step 5.5.1: Health Checks
**Aufgabe:** `/health` Endpoint für Liveness/Readiness Probes
**Was du erstellen wirst:**
1. **DevExpressPdfHealthCheck.cs**
```csharp
public class DevExpressPdfHealthCheck : IHealthCheck
{
private readonly IPdfProcessor _processor;
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken ct)
{
try
{
// Dummy PDF validieren (Smoke Test)
byte[] dummyPdf = CreateMinimalPdf();
await _processor.ValidateAsync(dummyPdf);
return HealthCheckResult.Healthy("DevExpress PDF OK");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("DevExpress PDF failed", ex);
}
}
}
```
2. **Program.cs Registration:**
```csharp
builder.Services.AddHealthChecks()
.AddCheck<DevExpressPdfHealthCheck>("devexpress")
.AddCheck("self", () => HealthCheckResult.Healthy());
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready"); // Kubernetes Readiness
app.MapHealthChecks("/health/live"); // Kubernetes Liveness
```
**Wo:** `API/HealthChecks/DevExpressPdfHealthCheck.cs`
---
#### ? Step 5.5.2: Polly Resilience
**Aufgabe:** Retry + Circuit Breaker + Timeout für DevExpress Calls
**Was du erstellen wirst:**
1. **DevExpressPdfProcessor erweitern (Polly Policies):**
```csharp
public class DevExpressPdfProcessor : IPdfProcessor
{
private readonly IAsyncPolicy _retryPolicy;
private readonly IAsyncPolicy _circuitBreakerPolicy;
private readonly IAsyncPolicy _timeoutPolicy;
public DevExpressPdfProcessor()
{
// Retry: 3x mit Exponential Backoff
_retryPolicy = Policy
.Handle<Exception>()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
// Circuit Breaker: Nach 5 Fehlern für 30 Sekunden öffnen
_circuitBreakerPolicy = Policy
.Handle<Exception>()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
// Timeout: 30 Sekunden max
_timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(30));
}
public async Task<PdfMetadata> ValidateAsync(byte[] pdfBytes)
{
// Alle Policies wrappen
return await _retryPolicy.ExecuteAsync(async () =>
await _circuitBreakerPolicy.ExecuteAsync(async () =>
await _timeoutPolicy.ExecuteAsync(async () =>
{
// DevExpress Call
using var processor = new PdfDocumentProcessor();
processor.LoadDocument(pdfBytes);
// ...
})));
}
}
```
2. **Logging für Resilience Events:**
```csharp
_retryPolicy = Policy
.Handle<Exception>()
.WaitAndRetryAsync(3,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (exception, timespan, retryCount, context) =>
{
Log.Warning("Retry {RetryCount} after {Delay}ms: {Exception}",
retryCount, timespan.TotalMilliseconds, exception.Message);
});
```
**Warum Polly?**
- ? **Retry:** DevExpress temporäre Fehler (File Lock, Memory)
- ? **Circuit Breaker:** DevExpress kaputt ? alle Requests sofort fehlschlagen (kein Retry-Storm)
- ? **Timeout:** DevExpress hängt ? Request-Timeout (nicht ewig warten)
---
### ? PHASE 6: Weitere Features (iterativ)
**Nach ValidatePdf (als Referenz):**
Jedes Feature folgt dem gleichen Pattern:
1. Interface erweitern (IPdfProcessor)
2. Service implementieren (DevExpressPdfProcessor) + Test
3. Command/Query + Handler + Validator
4. Endpoint erstellen
5. Integration Test
**Features:**
- [ ] ExtractAttachments (synchron)
- [ ] ConcatenatePdfs (asynchron - siehe Phase 6.5!)
- [ ] ApplyStamp (synchron)
- [ ] EmbedCertificate (synchron)
---
### ? PHASE 6.5: Async Processing (In-Memory Queue-based) - **NEW!**
**Ziel:** Große Operationen asynchron verarbeiten (> 5 Sekunden)
**Warum?**
- ConcatenatePdfs von 50 PDFs = 10+ Sekunden
- Client wartet nicht ? HTTP Timeout
- Queue-basiert = skalierbar (Background Worker kann parallel verarbeiten)
---
#### ? Step 6.5.1: In-Memory Queue Setup
**Aufgabe:** Queue für Async Jobs (In-Memory, keine Cloud-Abhängigkeiten)
**Was du erstellen wirst:**
1. **IJobQueue Interface (Application):**
```csharp
public interface IJobQueue
{
Task<JobId> EnqueueAsync<T>(T jobData) where T : class;
Task<JobStatus> GetStatusAsync(JobId jobId);
}
public record JobStatus(
JobId JobId,
ProcessingStatus Status,
int Progress,
string? ResultFilePath,
string? ErrorMessage);
```
2. **InMemoryJobQueue Implementation (Infrastructure):**
```csharp
public class InMemoryJobQueue : IJobQueue
{
private readonly ConcurrentQueue<JobData> _queue = new();
private readonly ConcurrentDictionary<string, JobStatus> _jobStatuses = new();
public async Task<JobId> EnqueueAsync<T>(T jobData) where T : class
{
var jobId = JobId.Create(Guid.NewGuid().ToString());
// Job Message in Queue
_queue.Enqueue(new JobData
{
JobId = jobId,
Data = jobData,
Type = typeof(T)
});
// Job Status setzen (Pending)
_jobStatuses[jobId.Value] = new JobStatus(
jobId,
ProcessingStatus.Pending,
Progress: 0,
ResultFilePath: null,
ErrorMessage: null
);
return jobId;
}
public Task<JobStatus> GetStatusAsync(JobId jobId)
{
_jobStatuses.TryGetValue(jobId.Value, out var status);
return Task.FromResult(status ?? throw new NotFoundException($"Job {jobId.Value} not found"));
}
public bool TryDequeue(out JobData jobData)
{
return _queue.TryDequeue(out jobData);
}
public void UpdateStatus(JobId jobId, JobStatus status)
{
_jobStatuses[jobId.Value] = status;
}
}
```
---
#### ? Step 6.5.2: Background Worker (Job Processor)
**Aufgabe:** IHostedService für Job-Verarbeitung
**Was du erstellen wirst:**
1. **JobProcessorService.cs (Infrastructure/BackgroundServices/):**
```csharp
public class JobProcessorService : BackgroundService
{
private readonly InMemoryJobQueue _jobQueue;
private readonly IPdfProcessor _pdfProcessor;
private readonly IFileStorage _fileStorage;
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
// Queue Message abrufen
if (_jobQueue.TryDequeue(out var jobData))
{
try
{
// Job Status: Processing
_jobQueue.UpdateStatus(jobData.JobId, new JobStatus(
jobData.JobId,
ProcessingStatus.Processing,
Progress: 0,
ResultFilePath: null,
ErrorMessage: null
));
// PDF-Operation (z.B. Concatenate)
var result = await _pdfProcessor.ConcatenateAsync(jobData.Data.PdfFiles);
// Ergebnis in lokalem Temp-Ordner speichern
var resultPath = await _fileStorage.SaveAsync(result, $"results/{jobData.JobId}.pdf");
// Job Status: Success
_jobQueue.UpdateStatus(jobData.JobId, new JobStatus(
jobData.JobId,
ProcessingStatus.Success,
Progress: 100,
ResultFilePath: resultPath,
ErrorMessage: null
));
}
catch (Exception ex)
{
// Job Status: Failed
_jobQueue.UpdateStatus(jobData.JobId, new JobStatus(
jobData.JobId,
ProcessingStatus.Failed,
Progress: 0,
ResultFilePath: null,
ErrorMessage: ex.Message
));
}
}
await Task.Delay(TimeSpan.FromMilliseconds(100), ct); // Polling-Interval
}
}
}
```
2. **Program.cs Registration:**
```csharp
builder.Services.AddHostedService<JobProcessorService>();
```
---
#### ? Step 6.5.3: Async Endpoints
**Aufgabe:** POST ? JobId, GET ? JobStatus
**Was du erstellen wirst:**
1. **POST /api/v1/documents/concatenate (Async):**
```csharp
app.MapPost("/api/v1/documents/concatenate", async (
ConcatenateRequest request,
IJobQueue jobQueue,
CancellationToken ct) =>
{
var jobId = await jobQueue.EnqueueAsync(new ConcatenateJobData
{
PdfFiles = request.PdfFiles,
TenantId = tenantContext.TenantId
});
return Results.Accepted($"/api/v1/jobs/{jobId}", new { jobId, status = "Pending" });
});
```
2. **GET /api/v1/jobs/{jobId}:**
```csharp
app.MapGet("/api/v1/jobs/{jobId}", async (
string jobId,
IJobQueue jobQueue,
CancellationToken ct) =>
{
var status = await jobQueue.GetStatusAsync(JobId.Create(jobId));
return Results.Ok(status);
});
```
**Flow:**
```
Client: POST /api/v1/documents/concatenate
? API: { "jobId": "abc123", "status": "Pending" } (HTTP 202 Accepted)
Background Worker: Verarbeitet Job aus Queue
? Status-Update: Processing (Progress: 50%)
Client: GET /api/v1/jobs/abc123
? API: { "jobId": "abc123", "status": "Processing", "progress": 50 }
Background Worker: Job fertig
? Status-Update: Success (ResultFilePath: "C:\\Temp\\DocumentOperator\\results\\abc123.pdf")
Client: GET /api/v1/jobs/abc123
? API: { "jobId": "abc123", "status": "Success", "resultUrl": "/download/abc123" }
Client: GET /download/abc123
? API: PDF-Datei aus lokalem Temp-Ordner
```
---
### ? PHASE 7: Swagger & API Documentation - **UPDATED!**
**Ziel:** Produktionsreife API-Dokumentation
**Steps:**
#### ? Step 7.1: Swagger Configuration (API-Key Support)
**Aufgabe:** API-Key Header in Swagger UI
**Was du erstellen wirst:**
1. **SwaggerConfiguration.cs:**
```csharp
builder.Services.AddSwaggerGen(c =>
{
// API-Key Security
c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header,
Name = "X-API-Key",
Description = "Enter your API Key (from Tenant DB)"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "ApiKey" }
},
Array.Empty<string>()
}
});
// XML Comments
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
// Example Schemas
c.SchemaFilter<ExampleSchemaFilter>();
});
```
2. **XML Comments in Endpoints:**
```csharp
/// <summary>
/// Validates a PDF document and returns metadata
/// </summary>
/// <param name="request">PDF as Base64 string</param>
/// <returns>PDF metadata (page count, file size, etc.)</returns>
/// <response code="200">PDF is valid, metadata returned</response>
/// <response code="400">Invalid PDF or Base64 format</response>
/// <response code="500">Internal server error during validation</response>
app.MapPost("/api/v1/documents/validate", ValidatePdf)
.WithName("ValidatePdf")
.WithTags("Documents")
.WithOpenApi();
```
---
#### ? Step 7.2: Response Examples (Swashbuckle)
**Aufgabe:** Beispiel-Responses in Swagger
**Was du erstellen wirst:**
1. **ExampleSchemaFilter.cs:**
```csharp
public class ExampleSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (context.Type == typeof(ValidatePdfRequest))
{
schema.Example = new OpenApiObject
{
["base64Pdf"] = new OpenApiString("JVBERi0xLjQKJeLjz9MK...")
};
}
}
}
```
---
### ? PHASE 8: Multi-Tenancy (Database-based) - **UPDATED!**
**Ziel:** Mandantenfähigkeit mit DB + Redis
**Steps:**
#### ? Step 8.1: EF Core Setup (SQLite)
**Aufgabe:** Tenant-Datenbank
**Was du erstellen wirst:**
1. **TenantDbContext.cs:**
```csharp
public class TenantDbContext : DbContext
{
public DbSet<Tenant> Tenants { get; set; }
public DbSet<TenantSettings> TenantSettings { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Tenant>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
entity.Property(e => e.ApiKeyHash).IsRequired().HasMaxLength(500);
entity.HasOne(e => e.Settings).WithOne(e => e.Tenant).HasForeignKey<TenantSettings>(e => e.TenantId);
});
}
}
```
2. **EF Core Migration:**
```bash
dotnet ef migrations add InitialCreate --project Infrastructure --startup-project API
dotnet ef database update --project Infrastructure --startup-project API
```
---
#### ? Step 8.2: Tenant Resolution Middleware
**Aufgabe:** API-Key ? Tenant auflösen (mit Redis Cache)
**Was du erstellen wirst:**
1. **TenantResolutionMiddleware.cs:**
```csharp
public class TenantResolutionMiddleware
{
private readonly RequestDelegate _next;
private readonly IDistributedCache _cache; // Redis
private readonly ITenantRepository _tenantRepository;
public async Task InvokeAsync(HttpContext context)
{
var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault();
if (string.IsNullOrEmpty(apiKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new { error = "Missing X-API-Key header" });
return;
}
// Redis Cache Lookup
var cacheKey = $"tenant:{apiKey}";
var cachedTenant = await _cache.GetStringAsync(cacheKey);
Tenant tenant;
if (cachedTenant != null)
{
tenant = JsonSerializer.Deserialize<Tenant>(cachedTenant);
}
else
{
// DB Lookup (BCrypt Hash Vergleich)
tenant = await _tenantRepository.GetByApiKeyAsync(apiKey);
if (tenant == null)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new { error = "Invalid API Key" });
return;
}
// Redis Cache befüllen (TTL: 1 Stunde)
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(tenant), new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
});
}
if (!tenant.IsActive)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsJsonAsync(new { error = "Tenant is inactive" });
return;
}
// ITenantContext setzen (Scoped Service)
var tenantContext = context.RequestServices.GetRequiredService<ITenantContext>();
tenantContext.SetTenant(tenant);
await _next(context);
}
}
```
---
#### ? Step 8.3: Tenant Admin API (CRUD)
**Aufgabe:** Admin-Endpoints für Tenant-Management
**Was du erstellen wirst:**
1. **POST /api/v1/admin/tenants (Create Tenant):**
```csharp
app.MapPost("/api/v1/admin/tenants", async (CreateTenantRequest request, ITenantRepository repo) =>
{
var apiKey = GenerateApiKey(); // Zufälliger Key
var apiKeyHash = BCrypt.Net.BCrypt.HashPassword(apiKey);
var tenant = new Tenant
{
Id = Guid.NewGuid(),
Name = request.Name,
ApiKeyHash = apiKeyHash,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
await repo.AddAsync(tenant);
return Results.Created($"/api/v1/admin/tenants/{tenant.Id}", new
{
tenant.Id,
tenant.Name,
apiKey // Nur EINMAL zurückgeben! (Client muss speichern)
});
})
.RequireAuthorization("Admin"); // Nur für Admins!
```
---
### ? PHASE 9: File Storage (Lokale Temp-Ordner) - **NEW!**
**Ziel:** File Storage für Temp-Files, Logos, Zertifikate
**Warum lokale Temp-Ordner?**
- ? **Einfachheit:** Keine Cloud-Abhängigkeiten
- ? **Schnell:** Lokaler Dateizugriff (keine Netzwerk-Latenz)
- ? **Flexibel:** Bei Bedarf später zu Cloud migrierbar (IFileStorage bleibt!)
---
#### ? Step 9.1: IFileStorage Interface
**Aufgabe:** Abstraction für File Storage
**Was du erstellen wirst:**
1. **IFileStorage.cs (Application/Common/Interfaces/):**
```csharp
public interface IFileStorage
{
Task<string> SaveAsync(byte[] content, string filename);
Task<byte[]> GetAsync(string path);
Task DeleteAsync(string path);
Task<bool> ExistsAsync(string path);
}
```
---
#### ? Step 9.2: LocalFileStorage Implementation
**Aufgabe:** Lokaler File Storage Provider
**Was du erstellen wirst:**
1. **LocalFileStorage.cs (Infrastructure/Services/FileStorage/):**
```csharp
public class LocalFileStorage : IFileStorage
{
private readonly string _basePath;
public LocalFileStorage(FileStorageSettings settings)
{
_basePath = settings.TempFolderPath ?? Path.Combine(Directory.GetCurrentDirectory(), "TempFiles");
Directory.CreateDirectory(_basePath);
}
public async Task<string> SaveAsync(byte[] content, string filename)
{
var path = Path.Combine(_basePath, filename);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllBytesAsync(path, content);
return path;
}
public async Task<byte[]> GetAsync(string path)
{
// Pfad kann absolut oder relativ sein
var fullPath = Path.IsPathFullyQualified(path) ? path : Path.Combine(_basePath, path);
return await File.ReadAllBytesAsync(fullPath);
}
public Task DeleteAsync(string path)
{
var fullPath = Path.IsPathFullyQualified(path) ? path : Path.Combine(_basePath, path);
if (File.Exists(fullPath))
{
File.Delete(fullPath);
}
return Task.CompletedTask;
}
public Task<bool> ExistsAsync(string path)
{
var fullPath = Path.IsPathFullyQualified(path) ? path : Path.Combine(_basePath, path);
return Task.FromResult(File.Exists(fullPath));
}
}
```
2. **FileStorageSettings.cs (Infrastructure/Configuration/):**
```csharp
public class FileStorageSettings
{
public string TempFolderPath { get; set; } = "TempFiles"; // Default: ./TempFiles
public int CleanupIntervalHours { get; set; } = 24; // Default: täglich
public int FileRetentionHours { get; set; } = 24; // Default: 24h Aufbewahrung
}
```
3. **appsettings.json:**
```json
{
"FileStorage": {
"TempFolderPath": "C:\\Temp\\DocumentOperator", // Windows Pfad
"CleanupIntervalHours": 24,
"FileRetentionHours": 24
}
}
```
---
#### ? Step 9.3: Temp-File Cleanup Service
**Aufgabe:** IHostedService für tägliche Cleanup
**Was du erstellen wirst:**
1. **TempFileCleanupService.cs (Infrastructure/BackgroundServices/):**
```csharp
public class TempFileCleanupService : BackgroundService
{
private readonly IFileStorage _fileStorage;
private readonly FileStorageSettings _settings;
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
// Warten auf nächsten Cleanup-Zeitpunkt
await Task.Delay(TimeSpan.FromHours(_settings.CleanupIntervalHours), ct);
try
{
// Temp-Files älter als FileRetentionHours löschen
var tempFolderPath = _settings.TempFolderPath;
if (Directory.Exists(tempFolderPath))
{
var files = Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories);
var cutoffTime = DateTime.UtcNow.AddHours(-_settings.FileRetentionHours);
foreach (var file in files)
{
var fileInfo = new FileInfo(file);
if (fileInfo.CreationTimeUtc < cutoffTime)
{
await _fileStorage.DeleteAsync(file);
Log.Information("Deleted temp file: {Path} (Age: {Hours}h)",
file, (DateTime.UtcNow - fileInfo.CreationTimeUtc).TotalHours);
}
}
}
}
catch (Exception ex)
{
Log.Error(ex, "Error during temp file cleanup");
}
}
}
}
```
---
### ? PHASE 10: Logging & Monitoring - **NEW!**
**Ziel:** Production-Ready Logging
**Steps:**
#### ? Step 10.1: Correlation IDs
**Aufgabe:** Request-Tracking über alle Logs
**Was du erstellen wirst:**
1. **CorrelationIdMiddleware.cs:**
```csharp
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
context.TraceIdentifier = correlationId;
context.Response.Headers.Add("X-Correlation-ID", correlationId);
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await _next(context);
}
}
}
```
2. **LoggingBehavior (MediatR Pipeline):**
```csharp
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
var requestName = typeof(TRequest).Name;
Log.Information("Handling {RequestName}: {@Request}", requestName, request);
var stopwatch = Stopwatch.StartNew();
var response = await next();
stopwatch.Stop();
Log.Information("Handled {RequestName} in {ElapsedMs}ms", requestName, stopwatch.ElapsedMilliseconds);
return response;
}
}
```
---
#### ? Step 10.2: Serilog Configuration (Seq + File)
**Aufgabe:** Structured Logging Setup
**Was du erstellen wirst:**
1. **Program.cs Serilog Setup:**
```csharp
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.WriteTo.Console()
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
.WriteTo.Seq("http://localhost:5341") // Dev: Seq UI
.CreateLogger();
```
2. **appsettings.Production.json (File Logging):**
```json
{
"Serilog": {
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "C:\\Logs\\DocumentOperator\\log-.txt",
"rollingInterval": "Day"
}
}
]
}
}
```
---
### ? PHASE 11: Production Deployment - **UPDATED!**
**Ziel:** IIS Deployment + Production Configuration
**Steps:**
- [ ] appsettings.Production.json (Lokale Temp-Ordner, Redis, File Logging)
- [ ] IIS Web.config (Kestrel Settings)
- [ ] SSL/TLS Configuration
- [ ] Redis Connection String (Production) - OPTIONAL (In-Memory Cache Alternative)
- [ ] Shared Network Drive (bei Multi-Server Setup) - OPTIONAL (lokale Temp-Ordner für Single-Server)
- [ ] Health Checks für Load Balancer
- [ ] Rate-Limiting (Redis-based, pro Tenant) - OPTIONAL (In-Memory für Single-Server)
---
## ?? TESTING STRATEGY
### Test-Driven Development (TDD)
**Flow:**
1. **Red:** Test schreiben (schlägt fehl, weil Code noch nicht existiert)
2. **Green:** Code schreiben (Test wird grün)
3. **Refactor:** Code verbessern (Test bleibt grün)
**Warum TDD?**
- ? Tests als Dokumentation (wie wird es genutzt?)
- ? Tests als Safety Net (Refactoring ohne Angst)
- ? Besseres Design (testbarer Code = guter Code)
- ? Keine "vergessenen" Tests (Test kommt ZUERST)
---
### Test-Pyramide
```
/\
/ \ E2E Tests (wenige)
/ \
/------\ Integration Tests (einige)
/ \
/----------\ Unit Tests (viele)
/ \
```
**Konkret:**
- **Unit Tests (viele):**
- Value Objects (Base64String.Create() wirft Exception?)
- Handlers (ValidatePdfHandler ruft IPdfProcessor auf?)
- Services (DevExpressPdfProcessor gibt Metadata zurück?)
- **Integration Tests (einige):**
- Endpoints (HTTP POST ? 200 OK + JSON?)
- MediatR Pipeline (ValidationBehavior funktioniert?)
- **E2E Tests (wenige/keine):**
- Haben wir nicht (API ist selbst der "Top-Level")
---
### Test-Abdeckung
**Ziel:** >80% Code Coverage (aber nicht um jeden Preis!)
**Was testen:**
- ? Value Objects (Validierung)
- ? Handlers (Business Logic)
- ? Services (DevExpress Integration)
- ? Endpoints (HTTP Responses)
- ? Exception Middleware (Error Mapping)
**Was NICHT testen:**
- ? DTOs (keine Logik)
- ? Enums (keine Logik)
- ? Program.cs (Startup Code)
---
### Test-Naming Convention
**Pattern:** `MethodName_Scenario_ExpectedResult`
**Beispiele:**
```csharp
// Value Object Tests
[Fact]
public void Create_EmptyString_ThrowsDomainValidationException() { }
[Fact]
public void Create_ValidBase64_ReturnsBase64String() { }
// Handler Tests
[Fact]
public async Task Handle_ValidPdf_ReturnsPdfMetadata() { }
[Fact]
public async Task Handle_InvalidPdf_ThrowsPdfProcessingException() { }
// Endpoint Tests
[Fact]
public async Task POST_ValidatePdf_ValidPdf_Returns200() { }
[Fact]
public async Task POST_ValidatePdf_InvalidPdf_Returns400() { }
```
---
## ?? CURRENT STATUS
### ? Completed
- **Phase 1:** Foundation & Clean Architecture Setup ?
- Solution Structure ?
- Dependencies ?
- NuGet Packages ?
- Folder Structure ?
- Configuration (appsettings.json) ?
- Serilog Setup ?
- Program.cs Setup ?
- **Phase 2:** Domain Layer (Minimal) ?
- ? Step 2.1 - Domain Exceptions (4 Exceptions erstellt)
- `DomainException.cs`
- `DomainValidationException.cs`
- `NotFoundException.cs`
- `PdfProcessingException.cs`
- ? Step 2.2 - Enums (DocumentOperationType, ProcessingStatus)
- ? Step 2.3 - Value Objects (Base64String, TenantId, PdfMetadata)
- **Phase 3:** Infrastructure Layer (Outside-In!)
- ? Step 3.1 - IPdfProcessor Interface erstellt
- ?? Step 3.2 - DevExpressPdfProcessor implementieren (TDD - **IN PROGRESS**)
- ? Step 3.2.1 - ProcessDocument Ordner gelöscht (Application Layer cleanup)
- ? Step 3.2.2 - Test-Ordnerstruktur erstellt (Unit/Infrastructure/Services/PdfProcessing)
- ? Step 3.2.3 - Test-PDF Datei hinzugefügt (valid.pdf als Embedded Resource)
- ? Step 3.2.4 - DevExpressPdfProcessorTests.cs erstellt (TDD Red Phase - 6 Tests)
### ?? In Progress
- **Phase 3, Step 3.2:** DevExpressPdfProcessor (TDD)
- **NEXT:** Step 3.2.5 - DevExpressPdfProcessor.cs implementieren (TDD Green Phase)
- **Progress:** 4/7 Mini-Steps abgeschlossen
### ? Pending
- **Phase 3:** Infrastructure Layer
- Step 3.2 - DevExpressPdfProcessor Implementation (mit Polly Resilience!)
- **Phase 4:** Application Layer
- Step 4.1 - MediatR Setup (DependencyInjection.cs, ValidationBehavior.cs, LoggingBehavior.cs)
- Step 4.2 - ValidatePdf Feature (Query, Handler, Validator)
- **Phase 5:** API Layer
- Step 5.1 - Exception Handling Middleware
- Step 5.2 - Minimal API Endpoint
- Step 5.3 - Integration Test
- **Phase 5.5:** Health Checks & Resilience (**NEU!**)
- Step 5.5.1 - Health Checks (DevExpressPdfHealthCheck)
- Step 5.5.2 - Polly Resilience (Retry, Circuit Breaker, Timeout)
- **Phase 6:** Weitere Features (synchron)
- ExtractAttachments
- ApplyStamp
- EmbedCertificate
- **Phase 6.5:** Async Processing (**NEU!**)
- Step 6.5.1 - In-Memory Queue Setup (IJobQueue Interface + InMemoryJobQueue)
- Step 6.5.2 - Background Worker (JobProcessorService - IHostedService)
- Step 6.5.3 - Async Endpoints (POST ? JobId, GET ? Status)
- **Phase 7:** Swagger & API Documentation (erweitert!)
- Step 7.1 - Swagger Configuration (API-Key Support, XML Comments)
- Step 7.2 - Response Examples (Swashbuckle)
- **Phase 8:** Multi-Tenancy (DB-based!) (**NEU!**)
- Step 8.1 - EF Core Setup (SQLite, Migrations)
- Step 8.2 - Tenant Resolution Middleware (Redis Cache)
- Step 8.3 - Tenant Admin API (CRUD)
- **Phase 9:** File Storage (**NEU!**)
- Step 9.1 - IFileStorage Interface
- Step 9.2 - LocalFileStorage Implementation (lokale Temp-Ordner)
- Step 9.3 - Temp-File Cleanup Service (IHostedService)
- **Phase 10:** Logging & Monitoring (**NEU!**)
- Step 10.1 - Correlation IDs (Request-Tracking)
- Step 10.2 - Serilog Configuration (Seq + File Logging)
- **Phase 11:** Production Deployment
- appsettings.Production.json
- IIS Deployment
- Rate-Limiting (Redis-based)
### ?? Hinweise zum aktuellen Stand
1. **Infrastructure Services:**
- Ordner existieren (PdfProcessing, FileStorage, DocumentValidation)
- **Aber:** Alle leer
- ?? **Action:** DevExpressPdfProcessor.cs implementieren (Step 3.2 - IN PROGRESS)
2. **DevExpress Universal License:**
- ? **Verfügbar!** Wir können alle DevExpress Pakete nutzen
- Aktuell nur: `DevExpress.Pdf.Core`
- Bei Bedarf können weitere Pakete hinzugefügt werden
---
## ?? KEY LEARNINGS & DECISIONS
### 1. Domain Layer minimal halten
**Entscheidung:** Nur Enums + Value Objects + Exceptions (ABER: EF Core Entities in Infrastructure!)
**Warum:**
- Domain = Business-Konzepte (technologie-unabhängig)
- Tenant/TenantSettings sind **Infrastructure** Entities (EF Core, Navigation Properties)
- Service-Anwendung (nicht Domain-lastig)
- YAGNI (You Ain't Gonna Need It)
**Was bedeutet das?**
- Domain/ValueObjects/TenantId.cs ? Value Object (immutable, validierend)
- Infrastructure/Data/Entities/Tenant.cs ? EF Core Entity (mit Navigation Properties)
- Domain kennt KEINE EF Core Dependencies!
**Alternative wäre gewesen:**
- Volle Domain Models (PdfDocument, DocumentAttachment, etc.)
- **Nachteile:** Overengineering, unnötige Komplexität
---
### 2. Outside-In Development
**Entscheidung:** Infrastructure ? Application ? API
**Warum:**
- Wir sehen **echten** Code sofort (DevExpress Integration)
- Keine Spekulation (wir wissen welche Exceptions geworfen werden)
- Schnellerer Feedback-Loop
**Alternative wäre gewesen:**
- Domain ? Application ? Infrastructure ? API
- **Nachteile:** Viel "spekulativer" Code ohne echte Implementation
---
### 3. Exception-based Error Handling
**Entscheidung:** Keine Result Pattern Library
**Warum:**
- Einfacherer Code (kein Result<T> Boilerplate)
- Zentrales Error Handling (Middleware)
- Standard .NET Exception-Flow
**Alternative wäre gewesen:**
- Ardalis.Result oder FluentResults
- **Nachteile:** Extra Package, mehr Boilerplate
---
### 4. TDD (Test-Driven Development)
**Entscheidung:** Test ? Code ? Refactor
**Warum:**
- Besseres Design (testbarer Code)
- Tests als Dokumentation
- Safety Net für Refactoring
**Alternative wäre gewesen:**
- Code ? Test (Test-After)
- **Nachteile:** Tests werden oft vergessen, schlechteres Design
---
### 5. Vertical Slice Architecture
**Entscheidung:** Pro Feature alles zusammen
**Warum:**
- Zusammengehöriger Code ist zusammen
- Einfacher zu finden und zu ändern
- Besser für Teams (weniger Merge-Konflikte)
**Alternative wäre gewesen:**
- Horizontal Layers (Commands/, Handlers/, Validators/)
- **Nachteile:** Code über viele Ordner verteilt
---
### 6. Multi-Tenancy: Database-based (statt appsettings.json) - **NEU!**
**Entscheidung:** EF Core + SQLite für Tenant-Management
**Warum:**
- **Skalierbar:** Neue Tenants ohne Neustart
- **Security:** API-Key Hashing (BCrypt, NICHT Klartext!)
- **Audit-Log:** LastUsedAt pro Tenant
- **Rate-Limiting:** Redis Counter pro Tenant
- **Redis Cache:** API-Key Lookups cached (Performance!)
**Alternative wäre gewesen:**
- appsettings.json mit API-Keys
- **Nachteile:** Nicht skalierbar, keine Rotation, kein Audit-Log
---
### 7. Async Processing: In-Memory Queue-based (für große Operationen) - **NEU!**
**Entscheidung:** In-Memory Queue + Background Worker
**Warum:**
- ConcatenatePdfs von 50 PDFs = 10+ Sekunden
- Synchron = HTTP Timeout
- In-Memory Queue = einfach, keine Cloud-Abhängigkeiten
- ProcessingStatus Enum wird jetzt genutzt!
**Alternative wäre gewesen:**
- Alle Operationen synchron
- **Nachteile:** HTTP Timeouts, nicht skalierbar
**Hinweis:**
- In-Memory Queue = nicht persistent (bei Server-Neustart gehen Jobs verloren)
- Für Production später: Redis Queue oder RabbitMQ (aber IJobQueue bleibt!)
- Für Single-Server Setup: In-Memory Queue ausreichend
---
### 8. File Storage: Lokale Temp-Ordner (statt Cloud) - **NEU!**
**Entscheidung:** IFileStorage Interface + LocalFileStorage
**Warum:**
- **Einfachheit:** Keine Cloud-Abhängigkeiten (Azure Blob Storage)
- **Schnell:** Lokaler Dateizugriff (keine Netzwerk-Latenz)
- **Flexibel:** IFileStorage Abstraction bleibt (später zu Cloud migrierbar!)
- **TempFileCleanupService:** Automatische Cleanup (täglich)
**Alternative wäre gewesen:**
- Cloud Storage (Azure Blob, AWS S3)
- **Nachteile:** Cloud-Abhängigkeit, Kosten, Komplexität
**Hinweis:**
- Für Multi-Server Setup: Shared Network Drive (UNC-Pfad) statt lokale Ordner
- IFileStorage Interface bleibt gleich (austauschbar!)
---
### 9. Resilience: Polly (Retry, Circuit Breaker, Timeout) - **NEU!**
**Entscheidung:** Polly für DevExpress Calls
**Warum:**
- **Retry:** Transient Errors (File Lock, Memory)
- **Circuit Breaker:** DevExpress kaputt ? alle Requests sofort fehlschlagen
- **Timeout:** DevExpress hängt ? nicht ewig warten
**Alternative wäre gewesen:**
- Keine Resilience
- **Nachteile:** Produktionsausfälle bei transient errors
---
### 10. Health Checks: FRÜH implementieren (Phase 5.5, nicht Phase 9) - **NEU!**
**Entscheidung:** Health Checks direkt nach erstem Endpoint
**Warum:**
- **Pflicht** für Production (Load Balancer, Kubernetes)
- `/health` Endpoint = Liveness/Readiness Probes
- DevExpressPdfHealthCheck = Smoke Test (PDF validieren)
**Alternative wäre gewesen:**
- Health Checks in Phase 9 (Production-Ready)
- **Nachteile:** Zu spät! Load Balancer braucht Health Checks sofort
---
### 11. Logging: Correlation IDs + Seq + File Logging - **NEU!**
**Entscheidung:** Structured Logging mit Correlation IDs
**Warum:**
- **Correlation IDs:** Request-Tracking über alle Logs (Debugging leichter)
- **Seq:** Log-Browsing UI (Development)
- **File Logging:** Production Logs (keine Cloud-Abhängigkeit)
- **LoggingBehavior:** MediatR Pipeline Behavior (automatisches Logging)
**Alternative wäre gewesen:**
- Nur Console Logging (keine Correlation IDs)
- **Nachteile:** Debugging schwierig, keine Request-Zusammenhänge, keine persistente Log-Speicherung
### 5. Vertical Slice Architecture
**Entscheidung:** Pro Feature alles zusammen
**Warum:**
- Zusammengehöriger Code ist zusammen
- Einfacher zu finden und zu ändern
- Besser für Teams (weniger Merge-Konflikte)
**Alternative wäre gewesen:**
- Horizontal Layers (Commands/, Handlers/, Validators/)
- **Nachteile:** Code über viele Ordner verteilt
---
## ?? REFERENCES & BEST PRACTICES
### 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)
- [xUnit Documentation](https://xunit.net/)
- [FluentAssertions Documentation](https://fluentassertions.com/)
- [Polly Documentation](https://www.pollydocs.org/) - **NEU:** Resilience Patterns
- [EF Core Documentation](https://learn.microsoft.com/en-us/ef/core/) - **NEU:** ORM für Tenant-DB
- [Serilog Documentation](https://serilog.net/) - **NEU:** Structured Logging
- [Seq Documentation](https://docs.datalust.co/docs) - **NEU:** Log Browsing UI
### Best Practices Applied
- ? Clean Architecture (pragmatisch!)
- ? CQRS with MediatR
- ? Vertical Slice Architecture
- ? Value Objects (DDD)
- ? Exception-based Error Handling
- ? Minimal APIs (.NET 8)
- ? TDD (Test-Driven Development)
- ? **Resilience Patterns (Polly)** - **NEU:** Retry, Circuit Breaker, Timeout
- ? **Multi-Tenancy (Database-based)** - **NEU:** EF Core + SQLite + Redis Cache
- ? **Async Processing (In-Memory Queue)** - **NEU:** In-Memory Queue + Background Worker
- ? **File Storage Abstraction (Lokale Temp-Ordner)** - **NEU:** IFileStorage + LocalFileStorage
- ? **Correlation IDs** - **NEU:** Request-Tracking über alle Logs
- ? **Health Checks** - **NEU:** Load Balancer Support
- ? 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 + Seq + File Logging)
---
## ?? 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 | ? Phase 1 completed |
| 2024-XX-XX | Phase 2 | ? Step 2.1 completed - Domain Exceptions created |
| 17.01.2025 | Roadmap | ?? **ROADMAP komplett überarbeitet** (Pragmatisch, Outside-In, TDD) |
| 17.01.2025 | Phase 2 | ? Step 2.2 completed - Enums erstellt |
| 17.01.2025 | Phase 2 | ? Step 2.3 completed - Value Objects erstellt |
| 17.01.2025 | Phase 2 | ? **Phase 2 (Domain Layer) komplett abgeschlossen!** |
| 17.01.2025 | Phase 3 | ? Step 3.1 completed - IPdfProcessor Interface erstellt |
| 17.01.2025 | Roadmap | ?? **ROADMAP Status-Update** - Aktueller Projektstand dokumentiert |
| 17.01.2025 | Infrastructure | ?? **DevExpress Universal License** hinzugefügt - Vollzugriff auf alle Pakete |
| 17.01.2025 | Phase 3 | ?? **Step 3.2 gestartet** - DevExpressPdfProcessor (TDD) |
| 17.01.2025 | Application | ? Step 3.2.1 - ProcessDocument Ordner gelöscht (Cleanup) |
| 17.01.2025 | Tests | ? Step 3.2.2 - Test-Ordnerstruktur erstellt, UnitTest1.cs gelöscht |
| 17.01.2025 | Tests | ? Step 3.2.3 - Test-PDF (valid.pdf) als Embedded Resource hinzugefügt |
| 17.01.2025 | Tests | ? Step 3.2.4 - DevExpressPdfProcessorTests.cs erstellt (TDD Red - 6 Tests) |
| 17.01.2025 | Roadmap | ?? **ROADMAP MAJOR UPDATE** - Production-Ready Features hinzugefügt! |
| 17.01.2025 | Architecture | ? **Multi-Tenancy:** Database-based (EF Core + SQLite + Redis Cache) |
| 17.01.2025 | Architecture | ? **Async Processing:** In-Memory Queue-based + Background Worker |
| 17.01.2025 | Architecture | ? **File Storage:** Lokale Temp-Ordner mit IFileStorage Abstraction |
| 17.01.2025 | Architecture | ? **Resilience:** Polly (Retry, Circuit Breaker, Timeout) |
| 17.01.2025 | Architecture | ? **Health Checks:** Früh implementieren (Phase 5.5 statt Phase 9) |
| 17.01.2025 | Architecture | ? **Logging:** Correlation IDs + Seq + File Logging |
| 17.01.2025 | Technology Stack | ? **9 neue NuGet Packages hinzugefügt** (EF Core, Polly, BCrypt, Seq - OHNE Azure) |
| 17.01.2025 | Roadmap | ? **6 neue Phasen** (5.5, 6.5, 8, 9, 10, 11) - insgesamt 11 Phasen statt 9 |
| 17.01.2025 | Documentation | ? **10 Key Learnings & Decisions** dokumentiert (statt 5) |
| 17.01.2025 | Roadmap | ?? **Azure Services entfernt** - Lokale Temp-Ordner + In-Memory Queue stattdessen
| 22.06.2026 | Dokumentation | ? **Azure-Referenzen vollständig entfernt** - Alle Azure-Referenzen bereinigt (PROJECT_STATUS.md + ROADMAP.md)
---
**END OF ROADMAP**
*This document is a living document and will be updated as development progresses.*