The roadmap has been updated to reflect a shift towards a scalable, resilient, and production-ready architecture. Key changes include: - Multi-tenancy with EF Core, SQLite, and Redis Cache. - Async processing using Azure Storage Queue and workers. - File storage abstraction with Azure Blob and local storage. - Resilience with Polly (retry, circuit breaker, timeout). - Early health checks for Kubernetes readiness/liveness. - Enhanced logging with Correlation IDs, Seq, and App Insights. - Expanded roadmap to 11 phases with new production features. - Added Swagger updates for API-Key auth and response examples. - Introduced EF Core tenant management with CRUD operations. - Added background services for async jobs and temp cleanup. - Updated testing strategy for resilience and async processing. - Documented 11 key learnings and updated best practices.
77 KiB
?? DocumentOperator - Project Roadmap (Pragmatic Edition)
Last Updated: 17.01.2025 (Updated with Resilience, Async Processing, Multi-Tenancy DB) | 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: Queue-based (Azure Storage Queue + Background Worker) für große Operationen
- ? File Storage: Azure Blob Storage + IFileStorage Abstraction (Multi-Server fähig!)
- ? 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 + Application Insights
- ? 11 neue NuGet Packages: EF Core, Polly, Azure Storage, 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?
- Skalierbarkeit: Multi-Server Support (Azure Blob, Redis Cache)
- 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, Application Insights
- 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) ? Azure Storage 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 Azure Blob Storage gespeichert (Multi-Server!)
- 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 LogoBlobPath { get; set; } // Azure Blob Storage Pfad
public string CertificateBlobPath { get; set; } // Azure Blob Storage Pfad
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; // Azure Blob Storage
public async Task<byte[]> Handle(ApplyStampCommand command, CancellationToken ct)
{
// Tenant-spezifisches Logo aus DB Settings laden
var logoPath = _tenantContext.CurrentTenant.Settings.LogoBlobPath;
// Logo aus Azure Blob Storage 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 |
| Azure.Storage.Blobs | 12.22.3 | NEU: Azure Blob Storage (File Storage) |
| 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
- Azure Blob Storage: Multi-Server File Storage (Logos, Zertifikate, Temp-Files)
- 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 (Production: Application Insights)
- 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 (Azure Storage 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/
? ? ??? AzureBlobFileStorage.cs ? **NEU:** IFileStorage Implementation (Azure Blob)
? ? ??? LocalFileStorage.cs ? **NEU:** IFileStorage Implementation (Dev/Test)
? ??? Queue/
? ??? AzureStorageJobQueue.cs ? **NEU:** IJobQueue Implementation (Azure Storage 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
? ??? AzureBlobSettings.cs ? **NEU:** Azure Blob Storage Configuration
? ??? AzureQueueSettings.cs ? **NEU:** Azure Storage Queue Configuration
? ??? RedisSettings.cs ? **NEU:** Redis Cache Configuration
??? DependencyInjection.cs ? Service Registration
Was gehört hierher:
- ? DevExpress Integration (mit Polly Resilience!)
- ? File Storage: Azure Blob Storage + Local File Storage (Abstraction!)
- ? Queue: Azure Storage Queue für Async Processing
- ? Datenbank: EF Core + SQLite (Tenant-Management)
- ? Background Services: Job Processing, Temp-File Cleanup
- ? Options Pattern Classes (Settings)
- ? 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 (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 horizontal skalieren)
? Step 6.5.1: Azure Storage Queue Setup
Aufgabe: Queue für Async Jobs
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? ResultBlobPath, string? ErrorMessage); -
AzureStorageJobQueue Implementation (Infrastructure):
public class AzureStorageJobQueue : IJobQueue { private readonly QueueClient _queueClient; private readonly TableClient _tableClient; // Job Status Tracking public async Task<JobId> EnqueueAsync<T>(T jobData) { var jobId = JobId.Create(Guid.NewGuid().ToString()); // Job Message in Queue await _queueClient.SendMessageAsync(JsonSerializer.Serialize(jobData)); // Job Status in Table Storage (Pending) await _tableClient.AddEntityAsync(new JobStatusEntity { PartitionKey = jobId.Value, RowKey = jobId.Value, Status = ProcessingStatus.Pending }); return jobId; } }
? 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 QueueClient _queueClient; private readonly IPdfProcessor _pdfProcessor; private readonly IFileStorage _fileStorage; protected override async Task ExecuteAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { // Queue Message abrufen var message = await _queueClient.ReceiveMessageAsync(); if (message.Value != null) { // Job verarbeiten var jobData = JsonSerializer.Deserialize<ConcatenateJobData>(message.Value.Body); try { // PDF-Operation var result = await _pdfProcessor.ConcatenateAsync(jobData.PdfFiles); // Ergebnis in Blob Storage var blobPath = await _fileStorage.SaveAsync(result, $"results/{jobData.JobId}.pdf"); // Job Status: Success await UpdateJobStatusAsync(jobData.JobId, ProcessingStatus.Success, blobPath); // Message löschen await _queueClient.DeleteMessageAsync(message.Value.MessageId, message.Value.PopReceipt); } catch (Exception ex) { // Job Status: Failed await UpdateJobStatusAsync(jobData.JobId, ProcessingStatus.Failed, errorMessage: ex.Message); } } await Task.Delay(TimeSpan.FromSeconds(1), 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 (ResultBlobPath: "/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 Blob Storage
? 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 (Azure Blob) - NEW!
Ziel: Multi-Server fähiges File Storage
Steps:
? 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: Azure Blob Storage Implementation
Aufgabe: Azure Blob Storage Provider
Was du erstellen wirst:
-
AzureBlobFileStorage.cs (Infrastructure/Services/FileStorage/):
public class AzureBlobFileStorage : IFileStorage { private readonly BlobContainerClient _containerClient; public AzureBlobFileStorage(AzureBlobSettings settings) { var serviceClient = new BlobServiceClient(settings.ConnectionString); _containerClient = serviceClient.GetBlobContainerClient(settings.ContainerName); _containerClient.CreateIfNotExists(); } public async Task<string> SaveAsync(byte[] content, string filename) { var blobClient = _containerClient.GetBlobClient(filename); await blobClient.UploadAsync(new BinaryData(content), overwrite: true); return blobClient.Uri.ToString(); } public async Task<byte[]> GetAsync(string path) { var blobClient = _containerClient.GetBlobClient(path); var response = await blobClient.DownloadContentAsync(); return response.Value.Content.ToArray(); } public async Task DeleteAsync(string path) { var blobClient = _containerClient.GetBlobClient(path); await blobClient.DeleteIfExistsAsync(); } } -
LocalFileStorage.cs (für Dev/Test):
public class LocalFileStorage : IFileStorage { private readonly string _basePath; public LocalFileStorage() { _basePath = Path.Combine(Directory.GetCurrentDirectory(), "LocalStorage"); 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) { return await File.ReadAllBytesAsync(path); } public Task DeleteAsync(string path) { File.Delete(path); return Task.CompletedTask; } }
? 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; protected override async Task ExecuteAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { // Täglich um 3 Uhr nachts var now = DateTime.UtcNow; var next3AM = now.Date.AddDays(1).AddHours(3); var delay = next3AM - now; await Task.Delay(delay, ct); // Temp-Files älter als 24h löschen var tempFiles = await _fileStorage.ListAsync("temp/"); foreach (var file in tempFiles) { if (file.CreatedAt < DateTime.UtcNow.AddHours(-24)) { await _fileStorage.DeleteAsync(file.Path); Log.Information("Deleted temp file: {Path}", file.Path); } } } } }
? 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 (Application Insights):
{ "Serilog": { "WriteTo": [ { "Name": "ApplicationInsights", "Args": { "connectionString": "InstrumentationKey=..." } } ] } }
? PHASE 11: Production Deployment - UPDATED!
Ziel: IIS Deployment + Production Configuration
Steps:
- appsettings.Production.json (Azure Blob, Redis, Application Insights)
- IIS Web.config (Kestrel Settings)
- SSL/TLS Configuration
- Redis Connection String (Production)
- Azure Blob Storage Connection String
- Health Checks für Kubernetes/Load Balancer
- Rate-Limiting (Redis-based, pro Tenant)
?? 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 - Azure Storage Queue Setup
- Step 6.5.2 - Background Worker (JobProcessorService)
- 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 - Azure Blob Storage + Local File Storage
- Step 9.3 - Temp-File Cleanup Service
-
Phase 10: Logging & Monitoring (NEU!)
- Step 10.1 - Correlation IDs (Request-Tracking)
- Step 10.2 - Serilog Configuration (Seq + Application Insights)
-
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: Queue-based (für große Operationen) - NEU!
Entscheidung: Azure Storage Queue + Background Worker
Warum:
- ConcatenatePdfs von 50 PDFs = 10+ Sekunden
- Synchron = HTTP Timeout
- Queue = skalierbar (Worker horizontal skalieren)
- ProcessingStatus Enum wird jetzt genutzt!
Alternative wäre gewesen:
- Alle Operationen synchron
- Nachteile: HTTP Timeouts, nicht skalierbar
8. File Storage: Azure Blob (statt Local Files) - NEU!
Entscheidung: IFileStorage Interface + Azure Blob + Local (Dev)
Warum:
- Multi-Server: Load Balancer mit 3 API-Instanzen
- Shared Storage: Azure Blob = alle Server greifen auf gleiche Files zu
- Abstraction: LocalFileStorage für Dev/Test
- Automatic Cleanup: TempFileCleanupService (IHostedService)
Alternative wäre gewesen:
- Temp-Files auf lokalem Server
- Nachteile: Multi-Server nicht möglich, Disk voll nach 1 Monat
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 + Application Insights - NEU!
Entscheidung: Structured Logging mit Correlation IDs
Warum:
- Correlation IDs: Request-Tracking über alle Logs (Debugging leichter)
- Seq: Log-Browsing UI (Development)
- Application Insights: Production Monitoring (Azure)
- LoggingBehavior: MediatR Pipeline Behavior (automatisches Logging)
Alternative wäre gewesen:
- Nur File Logging (keine Correlation IDs)
- Nachteile: Debugging schwierig, keine Request-Zusammenhänge
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
- Azure Blob Storage Documentation - NEU: File Storage
- Azure Storage Queue Documentation - NEU: Async Processing
- 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 (Queue-based) - NEU: Azure Storage Queue + Background Worker
- ? File Storage Abstraction (Azure Blob) - NEU: Multi-Server fähig
- ? Correlation IDs - NEU: Request-Tracking über alle Logs
- ? Health Checks - NEU: Kubernetes/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 + Application Insights)
?? 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: Queue-based (Azure Storage Queue + Background Worker) |
| 17.01.2025 | Architecture | ? File Storage: Azure Blob Storage + 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 + Application Insights |
| 17.01.2025 | Technology Stack | ? 11 neue NuGet Packages hinzugefügt (EF Core, Polly, Azure Storage, BCrypt, Seq) |
| 17.01.2025 | Roadmap | ? 6 neue Phasen (5.5, 6.5, 8, 9, 10, 11) - insgesamt 11 Phasen statt 9 |
| 17.01.2025 | Documentation | ? 11 Key Learnings & Decisions dokumentiert (statt 5) |
END OF ROADMAP
This document is a living document and will be updated as development progresses.