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.
80 KiB
?? 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?
- ? Multi-Tenancy: Database-based (EF Core + SQLite + Redis Cache) statt appsettings.json
- ? Async Processing: In-Memory Queue-based + Background Worker für große Operationen
- ? File Storage: Lokale Temp-Ordner mit IFileStorage Abstraction
- ? Resilience: Polly (Retry, Circuit Breaker, Timeout) für DevExpress Calls
- ?? Health Checks: FRÜH implementieren (Phase 5.5 statt Phase 9)
- ? Logging: Correlation IDs + Seq + File Logging
- ? 9 neue NuGet Packages: EF Core, Polly, BCrypt, Seq, Correlation IDs
- ? 6 neue Phasen: 5.5, 6.5, 8, 9, 10, 11 (insgesamt 11 Phasen statt 9)
- ? 11 Key Learnings: Dokumentiert (statt 5)
- ? 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
- Project Overview
- Architecture & Design Decisions
- Development Philosophy
- Technology Stack
- Project Structure
- Development Roadmap
- Testing Strategy
- 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):
- Client sendet PDF als Base64 in JSON + API-Key Header
- Tenant Resolution Middleware validiert API-Key (DB-Lookup mit Redis Cache)
- API validiert Input (FluentValidation in MediatR Pipeline)
- Handler konvertiert PDF ? Byte-Array
- DevExpress Service führt Operation durch (mit Polly Retry/Circuit Breaker)
- Ergebnis wird in lokalem Temp-Ordner gespeichert
- 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:
-
Enums (DocumentOperationType, ProcessingStatus)
- Pure Business-Konzepte
- Technologie-unabhängig
- Wiederverwendbar über alle Layer
-
Value Objects (Base64String, TenantId, PdfMetadata)
- Typsicherheit (Base64String statt string)
- Selbst-validierend (Fehler werfen im Constructor)
- Immutable (keine Änderungen nach Erstellung)
-
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:
// 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:
- FluentValidation für Input-Validierung (DTO-Ebene)
- Domain Exceptions für fachliche Fehler
- Zentrale Exception Handling Middleware im API Layer
Warum Exception-basiert?
- ? Einfacherer Code (kein
if (result.IsSuccess)überall) - ? Weniger Boilerplate (kein Result 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:
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:
- HTTP Request kommt rein
- ASP.NET Core deserialisiert JSON ? DTO
- Endpoint ruft MediatR auf
- MediatR Pipeline: Validation ? Handler ? Response
- 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:
// 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:
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.Corekö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:
-
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
-
KISS (Keep It Simple, Stupid)
- ? Kein Overengineering
- ? Keine unnötigen Design Patterns
- ? Einfachster Code der funktioniert
-
Clean Architecture JA, aber pragmatisch
- ? Dependency Rule einhalten (wichtig!)
- ? Separation of Concerns (wichtig!)
- ? ABER: Nur Abstraktionen die wir wirklich brauchen
-
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)
-
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:
- Solution erstellt (4 Projekte)
- Dependencies korrekt (Clean Architecture Dependency Rule)
- NuGet Packages installiert
- Folder-Struktur erstellt
- appsettings.json konfiguriert
- Options Pattern Classes erstellt
- 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:
DomainException.cs(Basis-Exception)DomainValidationException.cs(Value Object Validierung)NotFoundException.cs(Resource nicht gefunden)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:
-
DocumentOperationType.cs erstellen
- Wo:
Domain/Models/Enums/DocumentOperationType.cs - Inhalt:
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
- Wo:
-
ProcessingStatus.cs erstellen
- Wo:
Domain/Models/Enums/ProcessingStatus.cs - Inhalt:
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
- Wo:
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:
Base64Stringstattstring - ? Validierung an einer Stelle (Constructor)
- ? Immutable (keine Änderungen nach Erstellung)
- ? Wiederverwendbar (in Domain, Application, Infrastructure)
Was du erstellt hast:
-
Base64String.cs ?
- Factory Method:
Create(string value) - Validierung: Gültiges Base64-Format
- Konvertierung:
ToByteArray(),FromByteArray(byte[]) - 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, 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:
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:
-
Test schreiben (Red)
[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); } -
Implementation schreiben (Green)
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, // ... ); } } -
Test grün machen
-
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:
DependencyInjection.cs(Application Layer)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:
-
ValidatePdfQuery.cs
public record ValidatePdfQuery(Base64String PdfContent) : IRequest<PdfMetadata>; -
ValidatePdfHandler.cs
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); } } -
ValidatePdfValidator.cs (FluentValidation)
public class ValidatePdfValidator : AbstractValidator<ValidatePdfQuery> { public ValidatePdfValidator() { RuleFor(x => x.PdfContent).NotNull(); } } -
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:
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:
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:
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:
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:
-
DevExpressPdfHealthCheck.cs
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); } } } -
Program.cs Registration:
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:
-
DevExpressPdfProcessor erweitern (Polly Policies):
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); // ... }))); } } -
Logging für Resilience Events:
_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:
- Interface erweitern (IPdfProcessor)
- Service implementieren (DevExpressPdfProcessor) + Test
- Command/Query + Handler + Validator
- Endpoint erstellen
- 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:
-
IJobQueue Interface (Application):
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); -
InMemoryJobQueue Implementation (Infrastructure):
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:
-
JobProcessorService.cs (Infrastructure/BackgroundServices/):
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 } } } -
Program.cs Registration:
builder.Services.AddHostedService<JobProcessorService>();
? Step 6.5.3: Async Endpoints
Aufgabe: POST ? JobId, GET ? JobStatus
Was du erstellen wirst:
-
POST /api/v1/documents/concatenate (Async):
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" }); }); -
GET /api/v1/jobs/{jobId}:
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:
-
SwaggerConfiguration.cs:
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>(); }); -
XML Comments in Endpoints:
/// <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:
- ExampleSchemaFilter.cs:
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:
-
TenantDbContext.cs:
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); }); } } -
EF Core Migration:
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:
- TenantResolutionMiddleware.cs:
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:
- POST /api/v1/admin/tenants (Create Tenant):
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:
- IFileStorage.cs (Application/Common/Interfaces/):
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:
-
LocalFileStorage.cs (Infrastructure/Services/FileStorage/):
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)); } } -
FileStorageSettings.cs (Infrastructure/Configuration/):
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 } -
appsettings.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:
- TempFileCleanupService.cs (Infrastructure/BackgroundServices/):
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:
-
CorrelationIdMiddleware.cs:
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); } } } -
LoggingBehavior (MediatR Pipeline):
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:
-
Program.cs Serilog Setup:
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(); -
appsettings.Production.json (File Logging):
{ "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:
- Red: Test schreiben (schlägt fehl, weil Code noch nicht existiert)
- Green: Code schreiben (Test wird grün)
- 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:
// 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.csDomainValidationException.csNotFoundException.csPdfProcessingException.cs
- ? Step 2.2 - Enums (DocumentOperationType, ProcessingStatus)
- ? Step 2.3 - Value Objects (Base64String, TenantId, PdfMetadata)
- ? Step 2.1 - Domain Exceptions (4 Exceptions erstellt)
-
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
-
Infrastructure Services:
- Ordner existieren (PdfProcessing, FileStorage, DocumentValidation)
- Aber: Alle leer
- ?? Action: DevExpressPdfProcessor.cs implementieren (Step 3.2 - IN PROGRESS)
-
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 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)
/healthEndpoint = 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)
- MediatR Documentation
- FluentValidation Docs
- DevExpress PDF API
- ASP.NET Core Minimal APIs
- RFC 7807 Problem Details
- xUnit Documentation
- FluentAssertions Documentation
- Polly Documentation - NEU: Resilience Patterns
- EF Core Documentation - NEU: ORM für Tenant-DB
- Serilog Documentation - NEU: Structured Logging
- Seq Documentation - 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.